Compare commits

57 Commits

Author SHA1 Message Date
4d24673107 Add files via upload 2025-08-21 13:39:03 +07:00
b6b701fa3b fix add and edit address 2025-08-20 00:52:56 +07:00
94bd32d6b0 Merge remote-tracking branch 'origin/master' 2025-08-19 07:32:25 +07:00
6baf4ee5ce fix add address 2025-08-19 07:27:48 +07:00
ff8654d12a fix wholesale price and edit profile 2025-08-18 16:01:07 +07:00
22122d631b Add files via upload 2025-08-15 07:16:45 +07:00
6efbcde784 fix 2025-08-15 05:29:54 +07:00
a6d5d10e78 fix address, edit address 2025-08-15 02:11:16 +07:00
f373035f8e fix address store, address user, add 2025-08-14 16:52:22 +07:00
8d9815d89f fix address store, address user, add 2025-08-14 16:47:11 +07:00
1f41b4681f fix address store, add checkbox, forget password 2025-08-14 13:21:13 +07:00
3d8e82e3b5 Merge branch 'screen-features' 2025-08-12 16:32:53 +07:00
d4eacf6c7c fix detail order, kecamata, bank name, checkbox tnc 2025-08-12 16:28:18 +07:00
7351f9c5b7 Merge remote-tracking branch 'origin/master' 2025-08-12 15:02:06 +07:00
9fefe1d818 update apps name 2025-08-12 15:01:50 +07:00
0a8dac4d23 update kecamatan dan bank name 2025-08-12 15:00:37 +07:00
77ea5ed90a top-up 2025-08-12 13:51:19 +07:00
c303b419ed Merge pull request #36
gracia
2025-08-12 02:24:22 +07:00
bad456f842 store status, product fixed, file compression 2025-08-12 02:23:39 +07:00
ed60528049 add account name 2025-08-12 00:44:35 +07:00
a9c2f9c103 fix validation file upliad 2025-08-11 22:47:00 +07:00
d08f5465d1 Merge pull request #35 from shaulascr/master
update
2025-08-11 20:40:16 +07:00
eab3884fd6 Merge remote-tracking branch 'origin/master' 2025-08-11 20:32:11 +07:00
930688f50d update postalcode 2025-08-11 20:31:45 +07:00
b70c671710 Aplikasi BisaUMKM fix 2025-08-07 10:32:58 +07:00
3ac0461c7c Delete unduh/bisaUMKM.txt 2025-08-07 01:46:47 +07:00
61e8e1fe3c BisaUMKM server 2025-08-07 01:44:51 +07:00
39aa079003 BisaUMKM local 2025-08-07 01:41:43 +07:00
577fb27495 Create bisaUMKM.txt 2025-08-07 01:40:03 +07:00
8b76077a77 Merge remote-tracking branch 'origin/master' 2025-08-07 01:16:45 +07:00
6194dca259 update count product, chat, address 2025-08-07 01:11:55 +07:00
97be7a8f62 Add files via upload
add apps
2025-08-05 16:44:20 +07:00
d6d27e1c61 Update README.md 2025-07-10 15:07:55 +07:00
5992539ef3 Update README.md 2025-07-10 15:05:25 +07:00
c6c1c9f348 Update README.md 2025-07-10 14:57:36 +07:00
7f01914cb0 Create README.md 2025-07-10 14:53:11 +07:00
f43d160fe1 fix bug 2025-07-10 12:10:43 +07:00
bbaf6ed45d fix bug 2025-07-10 12:05:45 +07:00
9299fcbad1 fix bug 2025-07-10 12:04:18 +07:00
8b2a092465 fix bug 2025-07-10 12:00:40 +07:00
adf324e15d fix bug 2025-07-10 11:58:14 +07:00
f7f198e46f update refresh change tab 2025-07-02 08:53:16 +07:00
331410eb98 update province cities subdistricts and villages 2025-07-02 01:40:41 +07:00
db38654159 Merge remote-tracking branch 'origin/master' 2025-06-25 17:25:18 +07:00
2423f45968 Merge pull request #34
store under review
2025-06-23 22:49:05 +07:00
f539bfb9f0 store under review 2025-06-23 22:48:37 +07:00
c2bed56bf5 Merge pull request #33
gracia
2025-06-23 11:11:33 +07:00
0b47d0beb8 store review 2025-06-23 11:11:03 +07:00
d38bdb77dd Merge remote-tracking branch 'origin/master' 2025-06-21 18:13:09 +07:00
019b469556 fix price and chat date 2025-06-21 18:12:54 +07:00
4401daa310 fix search 2025-06-21 18:12:54 +07:00
28b0d5b082 update display category list 2025-06-21 18:12:54 +07:00
b37848e513 update display product list and paid status 2025-06-21 18:12:54 +07:00
6659ba4288 update 2025-06-21 18:12:45 +07:00
88cf5f1457 Merge pull request #32
gracia
2025-06-21 16:56:22 +07:00
e4fb409097 all sells list 2025-06-21 15:16:29 +07:00
3c97b3b3de product change stock and price 2025-06-20 01:50:39 +07:00
150 changed files with 7157 additions and 1920 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.iml *.iml
*.log
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea/caches
@ -12,4 +13,4 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties /app/google-services.json

13
.idea/deviceManager.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

68
README.md Normal file
View File

@ -0,0 +1,68 @@
# E-Commerce Serang (Android App)
A mobile e-commerce platform built with **Kotlin** (Android) and a backend in **Express**, tailored for small businesses (UMKM). Supports browsing, ordering, chatting, and tracking — with **shipping cost calculation (RajaOngkir)** and **push notifications** via Firebase Cloud Messaging.
---
## Overview
This Android app includes:
- Account registration, login, and OTP verification
- Browsing products by category and store
- Cart and checkout orders
- Shipping cost estimation via RajaOngkir API
- Checkout order, tracking, and status updates
- Real-time buyerseller chat
- Store registration and product management
- Store Balance as active status
- Top up store balance
- Write rating and feedback for purchased products
- Push notifications for user activity
The app communicates with a custom backend server via REST API and WebSocket.
## Tech Stack
- MVVM architecture
- Retrofit for API communication
- Hilt for dependency injection
- Socket.IO client for real-time chat
- Firebase Cloud Messaging (FCM) for notifications
- Coroutines for async operations
- Glide for image loading
- ViewBinding for UI access
- LiveData and StateFlow for reactive UI
- RajaOngkir API integration for shipping cost
## Project Structure
- api/retrofit/
- data/
- di/
- ui/
- auth/
- home/
- cart/
- order/
- history/
- review/
- chat/
- profile/
- store/
- addProduct/
- sells/
- balance/
- review/
- product/
- notif/
- utils/
- google-services.json
## How to Run
1. Clone this project and open it in Android Studio
2. Add your `google-services.json` for Firebase (FCM)
3. Update the API base URL in the Retrofit client
5. Settings BASE_URL in your local.properties
4. Build and run on an emulator or physical device

3
app/.gitignore vendored
View File

@ -1 +1,2 @@
/build /build
google-services.json

View File

@ -124,7 +124,4 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.13.0")) implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics") implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-messaging-ktx") implementation("com.google.firebase:firebase-messaging-ktx")
} }

View File

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

View File

@ -29,6 +29,18 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ui.auth.ResetPassActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.StoreSuspendedActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.StoreOnReviewActivity"
android:exported="false" />
<activity
android:name=".ui.profile.mystore.review.ReviewActivity"
android:exported="false" />
<activity <activity
android:name=".ui.product.listproduct.ListCategoryActivity" android:name=".ui.product.listproduct.ListCategoryActivity"
android:exported="false" /> android:exported="false" />
@ -48,7 +60,7 @@
android:name=".ui.product.storeDetail.StoreDetailActivity" android:name=".ui.product.storeDetail.StoreDetailActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.auth.RegisterStoreActivity" android:name=".ui.profile.mystore.RegisterStoreActivity"
android:exported="false" android:exported="false"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
@ -76,12 +88,11 @@
<!-- android:name="androidx.startup.InitializationProvider" --> <!-- android:name="androidx.startup.InitializationProvider" -->
<!-- android:authorities="${applicationId}.androidx-startup" --> <!-- android:authorities="${applicationId}.androidx-startup" -->
<!-- tools:node="remove" /> --> <!-- tools:node="remove" /> -->
<service <!-- <service -->
android:name=".ui.notif.SimpleWebSocketService" <!-- android:name=".ui.notif.SimpleWebSocketService" -->
android:enabled="true" <!-- android:enabled="true" -->
android:exported="false" <!-- android:exported="false" -->
android:foregroundServiceType="dataSync" /> <!-- android:foregroundServiceType="dataSync" /> -->
<activity <activity
android:name=".ui.profile.mystore.chat.ChatStoreActivity" android:name=".ui.profile.mystore.chat.ChatStoreActivity"
android:exported="false" android:exported="false"

View File

@ -2,30 +2,20 @@ package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class City( data class CityResponse(
@SerializedName("city_id")
val cityId: String,
@SerializedName("city_name") @field:SerializedName("cities")
val cityName: String, val cities: List<City>,
@SerializedName("province_id") @field:SerializedName("message")
val provinceId: String, val message: String
@SerializedName("province")
val provinceName: String,
@SerializedName("type")
val type: String,
@SerializedName("postal_code")
val postalCode: String
) )
data class CityResponse( data class City(
@SerializedName("message")
val message: String,
@SerializedName("cities") @field:SerializedName("city_name")
val data: List<City> val cityName: String,
)
@field:SerializedName("city_id")
val cityId: String
)

View File

@ -2,12 +2,15 @@ package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CreateAddressRequest ( data class CreateAddressRequest(
@SerializedName("userId")
val userId: Int,
@SerializedName("latitude") @SerializedName("latitude")
val lat: Double? = null, val lat: Double,
@SerializedName("longitude") @SerializedName("longitude")
val long: Double? = null, val long: Double,
@SerializedName("street") @SerializedName("street")
val street: String, val street: String,
@ -16,26 +19,26 @@ data class CreateAddressRequest (
val subDistrict: String, val subDistrict: String,
@SerializedName("city_id") @SerializedName("city_id")
val cityId: Int, val cityId: String,
@SerializedName("province_id") @SerializedName("province_id")
val provId: Int, val provId: Int,
@SerializedName("postal_code") @SerializedName("postal_code")
val postCode: String? = null, val postCode: String,
@SerializedName("village_id")
val idVillage: String?, // nullable for now
@SerializedName("detail") @SerializedName("detail")
val detailAddress: String? = null, val detailAddress: String,
@SerializedName("user_id") @SerializedName("is_store_location")
val userId: Int, val isStoreLocation: Boolean,
@SerializedName("recipient") @SerializedName("recipient")
val recipient: String, val recipient: String,
@SerializedName("phone") @SerializedName("phone")
val phone: String, val phone: String
@SerializedName("is_store_location")
val isStoreLocation: Boolean
) )

View File

@ -90,7 +90,7 @@ data class OrdersItem(
val orderId: Int, val orderId: Int,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int, val cityId: String,
var displayStatus: String? = null var displayStatus: String? = null
) )

View File

@ -0,0 +1,8 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class ResetPassReq (
@SerializedName("emailOrPhone")
val emailOrPhone: String
)

View File

@ -0,0 +1,30 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class ReviewsItem(
@field:SerializedName("order_item_id")
val orderItemId: Int? = null,
@field:SerializedName("review_date")
val reviewDate: String? = null,
@field:SerializedName("user_image")
val userImage: String? = null,
@field:SerializedName("product_id")
val productId: Int? = null,
@field:SerializedName("rating")
val rating: Int? = null,
@field:SerializedName("review_text")
val reviewText: String? = null,
@field:SerializedName("product_name")
val productName: String? = null,
@field:SerializedName("username")
val username: String? = null
)

View File

@ -98,5 +98,5 @@ data class Store(
val storeDescription: String, val storeDescription: String,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )

View File

@ -0,0 +1,28 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
data class UpdateAddressReq(
@SerializedName("street")
val street: String? = "",
@SerializedName("subdistrict")
val subdistrict: String? = "",
@SerializedName("postal_code")
val postalCCode: String? = "",
@SerializedName("detail")
val detail: String? = "",
@SerializedName("city_id")
val cityId: String? = "",
@SerializedName("province_id")
val provId: String? = "",
@SerializedName("phone")
val phone: String? = ""
)

View File

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

View File

@ -12,10 +12,8 @@ data class ListProvinceResponse(
) )
data class ProvincesItem( data class ProvincesItem(
@field:SerializedName("province")
val province: String,
@field:SerializedName("province_id") @field:SerializedName("province_id")
val provinceId: String val provinceId: String,
@field:SerializedName("province")
val province: String
) )

View File

@ -56,7 +56,7 @@ data class Orders(
val orderItems: List<OrderListItemsItem>, val orderItems: List<OrderListItemsItem>,
@field:SerializedName("auto_completed_at") @field:SerializedName("auto_completed_at")
val autoCompletedAt: String? = null, val autoCompletedAt: String,
@field:SerializedName("is_store_location") @field:SerializedName("is_store_location")
val isStoreLocation: Boolean? = null, val isStoreLocation: Boolean? = null,
@ -119,7 +119,7 @@ data class Orders(
val orderId: Int, val orderId: Int,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )
data class OrderListItemsItem( data class OrderListItemsItem(

View File

@ -15,7 +15,7 @@ data class OrderListResponse(
data class OrderItemsItem( data class OrderItemsItem(
@field:SerializedName("review_id") @field:SerializedName("review_id")
val reviewId: Int? = null, val reviewId: Int,
@field:SerializedName("quantity") @field:SerializedName("quantity")
val quantity: Int, val quantity: Int,

View File

@ -0,0 +1,21 @@
package com.alya.ecommerce_serang.data.api.response.customer.order
import com.google.gson.annotations.SerializedName
data class SubdistrictResponse(
@field:SerializedName("subdistricts")
val subdistricts: List<SubdistrictsItem>,
@field:SerializedName("message")
val message: String
)
data class SubdistrictsItem(
@field:SerializedName("subdistrict_id")
val subdistrictId: String,
@field:SerializedName("subdistrict_name")
val subdistrictName: String
)

View File

@ -0,0 +1,24 @@
package com.alya.ecommerce_serang.data.api.response.customer.order
import com.google.gson.annotations.SerializedName
data class VillagesResponse(
@field:SerializedName("villages")
val villages: List<VillagesItem>,
@field:SerializedName("message")
val message: String
)
data class VillagesItem(
@field:SerializedName("village_id")
val villageId: String,
@field:SerializedName("village_name")
val villageName: String,
@field:SerializedName("postal_code")
val postalCode: String
)

View File

@ -82,7 +82,8 @@ data class Product(
data class CartItemCheckoutInfo( data class CartItemCheckoutInfo(
val cartItem: CartItemsItem, val cartItem: CartItemsItem,
val isWholesale: Boolean val isWholesale: Boolean,
val wholesalePrice: Int? = null
) )

View File

@ -114,7 +114,7 @@ data class Store(
val storeDescription: String, val storeDescription: String,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )
data class ShippingItem( data class ShippingItem(

View File

@ -0,0 +1,69 @@
package com.alya.ecommerce_serang.data.api.response.customer.profile
import com.google.gson.annotations.SerializedName
data class AddressDetailResponse(
@field:SerializedName("address")
val address: AddressDetail,
@field:SerializedName("message")
val message: String
)
data class AddressDetail(
@field:SerializedName("village_id")
val villageId: String?,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@field:SerializedName("latitude")
val latitude: String,
@field:SerializedName("province_name")
val provinceName: String?,
@field:SerializedName("subdistrict_id")
val subdistrictId: String?,
@field:SerializedName("city_name")
val cityName: String?,
@field:SerializedName("user_id")
val userId: Int,
@field:SerializedName("province_id")
val provinceId: String,
@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("village_name")
val villageName: String,
@field:SerializedName("postal_code")
val postalCode: String,
@field:SerializedName("longitude")
val longitude: String,
@field:SerializedName("city_id")
val cityId: String
)

View File

@ -4,15 +4,18 @@ import com.google.gson.annotations.SerializedName
data class AddressResponse( data class AddressResponse(
@field:SerializedName("addresses") @field:SerializedName("addresses")
val addresses: List<AddressesItem>, val addresses: List<AddressesItem>,
@field:SerializedName("message") @field:SerializedName("message")
val message: String val message: String
) )
data class AddressesItem( data class AddressesItem(
@field:SerializedName("village_id")
val villageId: String?,
@field:SerializedName("is_store_location") @field:SerializedName("is_store_location")
val isStoreLocation: Boolean, val isStoreLocation: Boolean,
@ -23,10 +26,10 @@ data class AddressesItem(
val userId: Int, val userId: Int,
@field:SerializedName("province_id") @field:SerializedName("province_id")
val provinceId: Int, val provinceId: String,
@field:SerializedName("phone") @field:SerializedName("phone")
val phone: String, val phone: String?,
@field:SerializedName("street") @field:SerializedName("street")
val street: String, val street: String,
@ -35,7 +38,7 @@ data class AddressesItem(
val subdistrict: String, val subdistrict: String,
@field:SerializedName("recipient") @field:SerializedName("recipient")
val recipient: String, val recipient: String?,
@field:SerializedName("id") @field:SerializedName("id")
val id: Int, val id: Int,
@ -50,5 +53,5 @@ data class AddressesItem(
val longitude: String, val longitude: String,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int val cityId: String
) )

View File

@ -0,0 +1,57 @@
package com.alya.ecommerce_serang.data.api.response.customer.profile
import com.google.gson.annotations.SerializedName
data class UpdateAddressResponse(
@field:SerializedName("address")
val address: Address,
@field:SerializedName("message")
val message: String
)
data class Address(
@field:SerializedName("village_id")
val villageId: String,
@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: String,
@field:SerializedName("phone")
val phone: Any,
@field:SerializedName("street")
val street: String,
@field:SerializedName("subdistrict")
val subdistrict: String,
@field:SerializedName("recipient")
val recipient: Any,
@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: String
)

View File

@ -35,7 +35,7 @@ data class Store(
val detail: String, val detail: String,
@SerializedName("is_store_location") val isStoreLocation: Boolean, @SerializedName("is_store_location") val isStoreLocation: Boolean,
@SerializedName("user_id") val userId: Int, @SerializedName("user_id") val userId: Int,
@SerializedName("city_id") val cityId: Int, @SerializedName("city_id") val cityId: String,
@SerializedName("province_id") val provinceId: Int, @SerializedName("province_id") val provinceId: Int,
val phone: String?, val phone: String?,
val recipient: String?, val recipient: String?,

View File

@ -0,0 +1,13 @@
package com.alya.ecommerce_serang.data.api.response.store.review
import com.alya.ecommerce_serang.data.api.dto.ReviewsItem
import com.google.gson.annotations.SerializedName
data class ProductReviewResponse(
@field:SerializedName("reviews")
val reviews: List<ReviewsItem?>? = null,
@field:SerializedName("message")
val message: String? = null
)

View File

@ -132,5 +132,5 @@ data class Orders(
val username: String? = null, val username: String? = null,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int? = null val cityId: String? = null
) )

View File

@ -129,5 +129,7 @@ data class OrdersItem(
val status: String? = null, val status: String? = null,
@field:SerializedName("city_id") @field:SerializedName("city_id")
val cityId: Int? = null val cityId: String? = null,
var displayStatus: String? = null
) )

View File

@ -15,10 +15,14 @@ class ApiConfig {
val loggingInterceptor = HttpLoggingInterceptor().apply { val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY level = HttpLoggingInterceptor.Level.BODY
//httplogginginterceptor ntuk debug dan monitoring request/response
} }
val authInterceptor = AuthInterceptor(tokenManager) val authInterceptor = AuthInterceptor(tokenManager)
// utk tambak token auth otomatis pada header
// Konfigurasi OkHttpClient
//Low-level HTTP client yang melakukan actual network request
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor) .addInterceptor(loggingInterceptor)
.addInterceptor(authInterceptor) .addInterceptor(authInterceptor)
@ -27,13 +31,17 @@ class ApiConfig {
.writeTimeout(300, TimeUnit.SECONDS) // 5 minutes .writeTimeout(300, TimeUnit.SECONDS) // 5 minutes
.build() .build()
// Konfigurasi Retrofit
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
//almat domain backend
.baseUrl(BuildConfig.BASE_URL) .baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
//gson convertes: mengkonversi JSON ke object Kotlin dan sebaliknya
.client(client) .client(client)
.build() .build()
return retrofit.create(ApiService::class.java) return retrofit.create(ApiService::class.java)
// retrofit : menyederhanakan HTTP Request dgn mengubah interface Kotlin di ApiService menjadi HTTP calls secara otomatis
} }
fun getUnauthenticatedApiService(): ApiService { fun getUnauthenticatedApiService(): ApiService {

View File

@ -19,6 +19,7 @@ import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.PaymentConfirmRequest import com.alya.ecommerce_serang.data.api.dto.PaymentConfirmRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem import com.alya.ecommerce_serang.data.api.dto.ReviewProductItem
import com.alya.ecommerce_serang.data.api.dto.SearchRequest import com.alya.ecommerce_serang.data.api.dto.SearchRequest
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
@ -36,6 +37,7 @@ import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ResetPassResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatHistoryResponse
import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse import com.alya.ecommerce_serang.data.api.response.chat.ChatListResponse
@ -53,28 +55,33 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityRespon
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.UpdateAddressResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -388,18 +395,8 @@ interface ApiService {
@PUT("mystore/edit") @PUT("mystore/edit")
suspend fun updateStoreProfileMultipart( suspend fun updateStoreProfileMultipart(
@Part("store_name") storeName: RequestBody, @Part("store_name") storeName: RequestBody,
@Part("store_status") storeStatus: RequestBody,
@Part("store_description") storeDescription: RequestBody, @Part("store_description") storeDescription: RequestBody,
@Part("is_on_leave") isOnLeave: RequestBody, @Part("is_on_leave") isOnLeave: RequestBody,
@Part("city_id") cityId: RequestBody,
@Part("province_id") provinceId: RequestBody,
@Part("street") street: RequestBody,
@Part("subdistrict") subdistrict: RequestBody,
@Part("detail") detail: RequestBody,
@Part("postal_code") postalCode: RequestBody,
@Part("latitude") latitude: RequestBody,
@Part("longitude") longitude: RequestBody,
@Part("user_phone") userPhone: RequestBody,
@Part("store_type_id") storeTypeId: RequestBody, @Part("store_type_id") storeTypeId: RequestBody,
@Part storeimg: MultipartBody.Part? @Part storeimg: MultipartBody.Part?
): Response<StoreDataResponse> ): Response<StoreDataResponse>
@ -451,6 +448,12 @@ interface ApiService {
@Body addressData: HashMap<String, Any?> @Body addressData: HashMap<String, Any?>
): Response<StoreAddressResponse> ): Response<StoreAddressResponse>
@PUT("profile/address/edit/{id}")
suspend fun updateAddress(
@Path("id") addressId: Int,
@Body params: Map<String, @JvmSuppressWildcards Any>
): Response<UpdateAddressResponse>
@POST("search") @POST("search")
suspend fun saveSearchQuery( suspend fun saveSearchQuery(
@Body searchRequest: SearchRequest @Body searchRequest: SearchRequest
@ -507,4 +510,28 @@ interface ApiService {
@GET("mystore/notification") @GET("mystore/notification")
suspend fun getNotifStore( suspend fun getNotifStore(
): Response<ListStoreNotifResponse> ): Response<ListStoreNotifResponse>
@GET("store/reviews")
suspend fun getStoreProductReview(
): Response<ProductReviewResponse>
@GET("subdistrict/{cityId}")
suspend fun getSubdistrict(
@Path("cityId") cityId: String
): Response<SubdistrictResponse>
@GET("villages/{subdistrictId}")
suspend fun getVillages(
@Path("subdistrictId") subdistrictId: String
): Response<VillagesResponse>
@POST("resetpass")
suspend fun postResetPass(
@Body request: ResetPassReq
): Response<ResetPassResponse>
@GET("profile/address/detail/{id}")
suspend fun getDetailAddress(
@Path("id") addressId: Int
): Response<AddressDetailResponse>
} }

View File

@ -1,131 +1,140 @@
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.City import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.Province import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.StoreAddress import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.UpdateAddressResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject import retrofit2.Response
class AddressRepository(private val apiService: ApiService) { class AddressRepository(private val apiService: ApiService) {
private val TAG = "AddressRepository" private val TAG = "AddressRepository"
suspend fun getProvinces(): List<Province> = withContext(Dispatchers.IO) { // suspend fun getProvinces(): List<Province> = withContext(Dispatchers.IO) {
Log.d(TAG, "getProvinces() called") // Log.d(TAG, "getProvinces() called")
try { // try {
val response = apiService.getProvinces() // val response = apiService.getProvinces()
Log.d(TAG, "getProvinces() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") // Log.d(TAG, "getProvinces() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
//
// // Log the raw response body for debugging
// val rawBody = response.raw().toString()
// Log.d(TAG, "Raw response: $rawBody")
//
// if (response.isSuccessful) {
// val responseBody = response.body()
// Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
//
// val provinces = responseBody?.data ?: emptyList()
// Log.d(TAG, "getProvinces() success, got ${provinces.size} provinces")
// return@withContext provinces
// } else {
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
// Log.e(TAG, "getProvinces() error: $errorBody")
// throw Exception("API Error (${response.code()}): $errorBody")
// }
// } catch (e: Exception) {
// Log.e(TAG, "Exception in getProvinces()", e)
// throw Exception("Network error: ${e.message}")
// }
// }
// Log the raw response body for debugging // suspend fun getCities(provinceId: String): List<City> = withContext(Dispatchers.IO) {
val rawBody = response.raw().toString() // Log.d(TAG, "getCities() called with provinceId: $provinceId")
Log.d(TAG, "Raw response: $rawBody") // try {
// val response = apiService.getCities(provinceId)
// Log.d(TAG, "getCities() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
//
// if (response.isSuccessful) {
// val responseBody = response.body()
// Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
//
// val cities = responseBody?.data ?: emptyList()
// Log.d(TAG, "getCities() success, got ${cities.size} cities")
// return@withContext cities
// } else {
// val errorBody = response.errorBody()?.string() ?: "Unknown error"
// Log.e(TAG, "getCities() error: $errorBody")
// throw Exception("API Error (${response.code()}): $errorBody")
// }
// } catch (e: Exception) {
// Log.e(TAG, "Exception in getCities()", e)
// throw Exception("Network error: ${e.message}")
// }
// }
if (response.isSuccessful) { suspend fun getProvinces(): Response<ProvinceResponse> {
val responseBody = response.body() return apiService.getProvinces()
Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
val provinces = responseBody?.data ?: emptyList()
Log.d(TAG, "getProvinces() success, got ${provinces.size} provinces")
return@withContext provinces
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getProvinces() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in getProvinces()", e)
throw Exception("Network error: ${e.message}")
}
} }
suspend fun getCities(provinceId: String): List<City> = withContext(Dispatchers.IO) { suspend fun getCities(provinceId: String): Response<CityResponse> {
Log.d(TAG, "getCities() called with provinceId: $provinceId") return apiService.getCities(provinceId)
try {
val response = apiService.getCities(provinceId)
Log.d(TAG, "getCities() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
if (response.isSuccessful) {
val responseBody = response.body()
Log.d(TAG, "Response body: ${Gson().toJson(responseBody)}")
val cities = responseBody?.data ?: emptyList()
Log.d(TAG, "getCities() success, got ${cities.size} cities")
return@withContext cities
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getCities() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody")
}
} catch (e: Exception) {
Log.e(TAG, "Exception in getCities()", e)
throw Exception("Network error: ${e.message}")
}
} }
suspend fun getStoreAddress(): StoreAddress? = withContext(Dispatchers.IO) { // suspend fun getStoreAddress(): StoreAddress? = withContext(Dispatchers.IO) {
Log.d(TAG, "getStoreAddress() called") // Log.d(TAG, "getStoreAddress() called")
try { // try {
val response = apiService.getStoreAddress() // val response = apiService.getStoreAddress()
Log.d(TAG, "getStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}") // Log.d(TAG, "getStoreAddress() response: isSuccessful=${response.isSuccessful}, code=${response.code()}")
//
if (response.isSuccessful) { // if (response.isSuccessful) {
val responseBody = response.body() // val responseBody = response.body()
val rawJson = Gson().toJson(responseBody) // val rawJson = Gson().toJson(responseBody)
Log.d(TAG, "Response body: $rawJson") // Log.d(TAG, "Response body: $rawJson")
//
val address = responseBody?.data // val address = responseBody?.data
Log.d(TAG, "getStoreAddress() success, address: $address") // Log.d(TAG, "getStoreAddress() success, address: $address")
//
// Convert numeric strings to proper types if needed // // Convert numeric strings to proper types if needed
address?.let { // address?.let {
// Handle city_id if it's a number // // Handle city_id if it's a number
if (it.cityId.isBlank() && rawJson.contains("city_id")) { // if (it.cityId.isBlank() && rawJson.contains("city_id")) {
try { // try {
val cityId = JSONObject(rawJson).getJSONObject("store").optInt("city_id", 0) // val cityId = JSONObject(rawJson).getJSONObject("store").optInt("city_id", 0)
if (cityId > 0) { // if (cityId > 0) {
it.javaClass.getDeclaredField("cityId").apply { // it.javaClass.getDeclaredField("cityId").apply {
isAccessible = true // isAccessible = true
set(it, cityId.toString()) // set(it, cityId.toString())
} // }
Log.d(TAG, "Updated cityId to: ${it.cityId}") // Log.d(TAG, "Updated cityId to: ${it.cityId}")
} // }
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(TAG, "Error parsing city_id", e) // Log.e(TAG, "Error parsing city_id", e)
} // }
} // }
//
// Handle province_id if it's a number // // Handle province_id if it's a number
if (it.provinceId.isBlank() && rawJson.contains("province_id")) { // if (it.provinceId.isBlank() && rawJson.contains("province_id")) {
try { // try {
val provinceId = JSONObject(rawJson).getJSONObject("store").optInt("province_id", 0) // val provinceId = JSONObject(rawJson).getJSONObject("store").optInt("province_id", 0)
if (provinceId > 0) { // if (provinceId > 0) {
it.javaClass.getDeclaredField("provinceId").apply { // it.javaClass.getDeclaredField("provinceId").apply {
isAccessible = true // isAccessible = true
set(it, provinceId.toString()) // set(it, provinceId.toString())
} // }
Log.d(TAG, "Updated provinceId to: ${it.provinceId}") // Log.d(TAG, "Updated provinceId to: ${it.provinceId}")
} // }
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(TAG, "Error parsing province_id", e) // Log.e(TAG, "Error parsing province_id", e)
} // }
} // }
} // }
//
return@withContext address // return@withContext address
} else { // } else {
val errorBody = response.errorBody()?.string() ?: "Unknown error" // val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "getStoreAddress() error: $errorBody") // Log.e(TAG, "getStoreAddress() error: $errorBody")
throw Exception("API Error (${response.code()}): $errorBody") // throw Exception("API Error (${response.code()}): $errorBody")
} // }
} catch (e: Exception) { // } catch (e: Exception) {
Log.e(TAG, "Exception in getStoreAddress()", e) // Log.e(TAG, "Exception in getStoreAddress()", e)
throw Exception("Network error: ${e.message}") // throw Exception("Network error: ${e.message}")
} // }
} // }
suspend fun saveStoreAddress( suspend fun saveStoreAddress(
provinceId: String, provinceId: String,
@ -171,4 +180,17 @@ class AddressRepository(private val apiService: ApiService) {
throw Exception("Network error: ${e.message}") throw Exception("Network error: ${e.message}")
} }
} }
suspend fun getStoreAddress(): Response<AddressResponse> {
return apiService.getAddress()
}
suspend fun updateAddress(addressId: Int, params: Map<String, Any>): Response<UpdateAddressResponse> {
return apiService.updateAddress(addressId, params)
}
suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
val response = apiService.getSubdistrict(cityId)
return if (response.isSuccessful) response.body() else null
}
} }

View File

@ -1,6 +1,7 @@
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.ProductsItem
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.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
@ -52,48 +53,49 @@ class MyStoreRepository(private val apiService: ApiService) {
suspend fun updateStoreProfile( suspend fun updateStoreProfile(
storeName: RequestBody, storeName: RequestBody,
storeStatus: RequestBody,
storeDescription: RequestBody, storeDescription: RequestBody,
isOnLeave: RequestBody, isOnLeave: RequestBody,
cityId: RequestBody,
provinceId: RequestBody,
street: RequestBody,
subdistrict: RequestBody,
detail: RequestBody,
postalCode: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
userPhone: RequestBody,
storeType: RequestBody, storeType: RequestBody,
storeimg: MultipartBody.Part? storeimg: MultipartBody.Part?
): Response<StoreDataResponse> { ): Response<StoreDataResponse>? {
return apiService.updateStoreProfileMultipart(
storeName, storeStatus, storeDescription, isOnLeave, cityId, provinceId, return try {
street, subdistrict, detail, postalCode, latitude, longitude, userPhone, storeType, storeimg Log.d(TAG, "storeName: $storeName")
) Log.d(TAG, "storeDescription: $storeDescription")
Log.d(TAG, "isOnLeave: $isOnLeave")
Log.d(TAG, "storeType: $storeType")
Log.d(TAG, "storeimg: ${storeimg?.headers}")
apiService.updateStoreProfileMultipart(
storeName, storeDescription, isOnLeave, storeType, storeimg
)
} catch (e: Exception) {
Log.e(TAG, "Error updating store profile", e)
null
}
} }
suspend fun getSellList(status: String): Result<OrderListResponse> { suspend fun getSellList(status: String): Result<OrderListResponse> {
return try { return try {
Log.d("SellsRepository", "Add Evidence : $status") Log.d(TAG, "Add Evidence : $status")
val response = apiService.getSellList(status) val response = apiService.getSellList(status)
if (response.isSuccessful) { if (response.isSuccessful) {
val allListSell = response.body() val allListSell = response.body()
if (allListSell != null) { if (allListSell != null) {
Log.d("SellsRepository", "Add Evidence successfully: ${allListSell.message}") Log.d(TAG, "Add Evidence successfully: ${allListSell.message}")
Result.Success(allListSell) Result.Success(allListSell)
} else { } else {
Log.e("SellsRepository", "Response body was null") Log.e(TAG, "Response body was null")
Result.Error(Exception("Empty response from server")) Result.Error(Exception("Empty response from server"))
} }
} else { } else {
val errorBody = response.errorBody()?.string() ?: "Unknown error" val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e("SellsRepository", "Error Add Evidence : $errorBody") Log.e(TAG, "Error Add Evidence : $errorBody")
Result.Error(Exception(errorBody)) Result.Error(Exception(errorBody))
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("SellsRepository", "Exception Add Evidence ", e) Log.e(TAG, "Exception Add Evidence ", e)
Result.Error(e) Result.Error(e)
} }
} }
@ -118,11 +120,29 @@ class MyStoreRepository(private val apiService: ApiService) {
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyStoreRepository", "Error fetching balance", e) Log.e(TAG, "Error fetching balance", e)
Result.Error(e) Result.Error(e)
} }
} }
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
return try {
val response = apiService.getStoreProduct()
if (response.isSuccessful) {
response.body()?.products?.filterNotNull() ?: emptyList()
} else {
throw Exception("Failed to fetch store products: ${response.message()}")
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching store products", e)
throw e
}
}
companion object {
private var TAG = "MyStoreRepository"
}
// private fun fetchBalance() { // private fun fetchBalance() {
// showLoading(true) // showLoading(true)
// lifecycleScope.launch { // lifecycleScope.launch {

View File

@ -20,12 +20,16 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityRespon
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailPaymentItem import com.alya.ecommerce_serang.data.api.response.customer.product.DetailPaymentItem
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem import com.alya.ecommerce_serang.data.api.response.customer.product.StoreItem
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.UpdateAddressResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
@ -294,6 +298,43 @@ class OrderRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null return if (response.isSuccessful) response.body() else null
} }
suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
val response = apiService.getSubdistrict(cityId)
return if (response.isSuccessful) response.body() else null
}
suspend fun getListVillages(subId: String): VillagesResponse? {
val response = apiService.getVillages(subId)
return if (response.isSuccessful) response.body() else null
}
suspend fun getAddressDetail(addressId: Int): AddressDetailResponse? {
return try {
Log.d("Order Repository", "Fetching address detail for ID: $addressId")
val response = apiService.getDetailAddress(addressId)
Log.d("Order Repository", "Response code: ${response.code()}")
Log.d("Order Repository", "Response message: ${response.message()}")
if (response.isSuccessful) {
val body = response.body()
Log.d("Order Repository", "Address detail response body: $body")
body
} else {
Log.w("Order Repository", "Failed to get address detail. Error body: ${response.errorBody()?.string()}")
null
}
} catch (e: Exception) {
Log.e("Order Repository", "Error getting address detail", e)
null
}
}
suspend fun updateAddress(addressId: Int, params: Map<String, Any>): Response<UpdateAddressResponse> {
return apiService.updateAddress(addressId, params)
}
suspend fun fetchUserProfile(): Result<UserProfile?> { suspend fun fetchUserProfile(): Result<UserProfile?> {
return try { return try {
val response = apiService.getUserProfile() val response = apiService.getUserProfile()
@ -319,6 +360,7 @@ class OrderRepository(private val apiService: ApiService) {
} }
} }
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> { suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
return try { return try {
Log.d("OrderRepository", "Uploading payment proof...") Log.d("OrderRepository", "Uploading payment proof...")

View File

@ -0,0 +1,50 @@
package com.alya.ecommerce_serang.data.repository
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.response.customer.product.ProductResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
class ReviewRepository(private val apiService: ApiService) {
suspend fun getReviewList(score: String): Result<ProductReviewResponse> {
return try {
val response = apiService.getStoreProductReview()
if (response.isSuccessful) {
val allReviews = response.body()
val filteredReviews = if (score == "all") {
allReviews
} else {
val targetScore = score.toIntOrNull()
allReviews?.copy(reviews = allReviews.reviews?.filter {
val rating = it?.rating ?: 0
when(targetScore) {
5 -> rating > 4
4 -> rating > 3 && rating <= 4
3 -> rating > 2 && rating <= 3
2 -> rating > 1 && rating <= 2
1 -> rating <= 1
else -> true
}
})
}
Result.Success(filteredReviews!!)
} else {
Result.Error(Exception("HTTP ${response.code()}: ${response.message()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun getProductDetail(productId: Int): ProductResponse? {
return try {
val response = apiService.getDetailProduct(productId)
if (response.isSuccessful) {
response.body()
} else null
} catch (e: Exception) {
null
}
}
}

View File

@ -7,6 +7,7 @@ import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
@ -18,9 +19,12 @@ import com.alya.ecommerce_serang.data.api.response.auth.NotifstoreItem
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ResetPassResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils import com.alya.ecommerce_serang.utils.FileUtils
@ -68,6 +72,16 @@ class UserRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null return if (response.isSuccessful) response.body() else null
} }
suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
val response = apiService.getSubdistrict(cityId)
return if (response.isSuccessful) response.body() else null
}
suspend fun getListVillages(subId: String): VillagesResponse? {
val response = apiService.getVillages(subId)
return if (response.isSuccessful) response.body() else null
}
suspend fun registerUser(request: RegisterRequest): RegisterResponse { suspend fun registerUser(request: RegisterRequest): RegisterResponse {
val response = apiService.register(request) // API call val response = apiService.register(request) // API call
@ -87,7 +101,7 @@ class UserRepository(private val apiService: ApiService) {
longitude: String, longitude: String,
street: String, street: String,
subdistrict: String, subdistrict: String,
cityId: Int, cityId: String,
provinceId: Int, provinceId: Int,
postalCode: Int, postalCode: Int,
detail: String, detail: String,
@ -266,6 +280,11 @@ class UserRepository(private val apiService: ApiService) {
val requestFile = compressedFile.asRequestBody(mimeType.toMediaTypeOrNull()) val requestFile = compressedFile.asRequestBody(mimeType.toMediaTypeOrNull())
Log.d(TAG, "$formName compressed size: ${compressedFile.length() / 1024} KB") Log.d(TAG, "$formName compressed size: ${compressedFile.length() / 1024} KB")
val compressedSizeMB = compressedFile.length().toDouble() / (1024 * 1024)
if (compressedSizeMB > 1) {
throw IllegalArgumentException("$formName lebih dari 1 MB setelah kompresi")
}
MultipartBody.Part.createFormData(formName, compressedFile.name, requestFile) MultipartBody.Part.createFormData(formName, compressedFile.name, requestFile)
} else { } else {
throw IllegalArgumentException("$formName harus berupa file gambar (JPEG, JPG, atau PNG)") throw IllegalArgumentException("$formName harus berupa file gambar (JPEG, JPG, atau PNG)")
@ -473,6 +492,30 @@ class UserRepository(private val apiService: ApiService) {
Result.Error(e) Result.Error(e)
} }
} }
suspend fun resetPassword(request: ResetPassReq): Result<ResetPassResponse>{
return try {
val response = apiService.postResetPass(request)
if (response.isSuccessful){
val resetPassResponse = response.body()
if (resetPassResponse != null) {
Result.Success(resetPassResponse)
}
else {
Result.Error(Exception("Empty response from server"))
}
}
else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Error RESET PASS address: $errorBody")
Result.Error(Exception(errorBody))
}
}
catch (e: Exception){
Result.Error(e)
}
}
companion object{ companion object{
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
} }

View File

@ -8,9 +8,7 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
@ -43,20 +41,18 @@ class LoginActivity : AppCompatActivity() {
setContentView(binding.root) setContentView(binding.root)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge() enableEdgeToEdge()
// Apply insets to your root layout // ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> // val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) // view.setPadding(
view.setPadding( // systemBars.left,
systemBars.left, // systemBars.top,
systemBars.top, // systemBars.right,
systemBars.right, // systemBars.bottom
systemBars.bottom // )
) // windowInsets
windowInsets // }
}
// onBackPressedDispatcher.addCallback(this) { // onBackPressedDispatcher.addCallback(this) {
// // Handle the back button event // // Handle the back button event
@ -86,6 +82,11 @@ class LoginActivity : AppCompatActivity() {
startActivity(Intent(this, RegisterActivity::class.java)) startActivity(Intent(this, RegisterActivity::class.java))
finish() finish()
} }
binding.tvForgetPassword.setOnClickListener {
startActivity(Intent(this, ResetPassActivity::class.java))
finish()
}
} }
private fun observeLoginState() { private fun observeLoginState() {
@ -105,6 +106,7 @@ class LoginActivity : AppCompatActivity() {
finish() finish()
} }
is com.alya.ecommerce_serang.data.repository.Result.Error -> { is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show() Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show()
} }
is Result.Loading -> { is Result.Loading -> {

View File

@ -43,24 +43,6 @@ class RegisterActivity : AppCompatActivity() {
setContentView(binding.root) setContentView(binding.root)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'") Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'")
Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'") Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'")
@ -104,7 +86,7 @@ class RegisterActivity : AppCompatActivity() {
} }
} }
// Function to navigate to the next fragment // navigate step register in fragment
fun navigateToStep(step: Int, userData: RegisterRequest?) { fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) { val fragment = when (step) {
1 -> RegisterStep1Fragment.newInstance() 1 -> RegisterStep1Fragment.newInstance()

View File

@ -0,0 +1,123 @@
package com.alya.ecommerce_serang.ui.auth
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityResetPassBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
class ResetPassActivity : AppCompatActivity() {
private val TAG = "ResetPassActivity"
private lateinit var binding: ActivityResetPassBinding
private val loginViewModel: LoginViewModel by viewModels{
BaseViewModelFactory {
val apiService = ApiConfig.getUnauthenticatedApiService()
val userRepository = UserRepository(apiService)
LoginViewModel(userRepository, this)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityResetPassBinding.inflate(layoutInflater)
setContentView(binding.root)
// enableEdgeToEdge()
setupToolbar()
setupUI()
}
private fun setupToolbar(){
binding.headerResetPass.headerLeftIcon.setOnClickListener{
finish()
}
binding.headerResetPass.headerTitle.text = "Lupa Password"
}
private fun setupUI(){
binding.btnReset.setOnClickListener {
val email = binding.etEmail.text.toString().trim()
if (email.isNotEmpty()) {
loginViewModel.resetPassword(email)
} else {
binding.etEmail.error = "Masukkan Email Anda"
}
}
}
private fun observeResetPassword() {
loginViewModel.resetPasswordState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
showLoading(true)
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
showLoading(false)
handleSuccess("Silahkan cek email anda untuk melihat password anda.")
Log.d(TAG, "Success rest password: ${result.data.message}")
}
is Result.Error -> {
showLoading(false)
handleError("Email anda salah atau tidak ditemukan.")
Log.e(TAG, "Error reset password ${result.exception.message}")
}
null -> {
// Initial state
}
}
}
}
private fun showLoading(isLoading: Boolean) {
if (isLoading) {
binding.progressBar.visibility = View.VISIBLE
binding.btnReset.isEnabled = false
binding.etEmail.isEnabled = false
} else {
binding.progressBar.visibility = View.GONE
binding.btnReset.isEnabled = true
binding.etEmail.isEnabled = true
}
}
private fun handleSuccess(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
// Show success dialog and navigate back to login
AlertDialog.Builder(this)
.setTitle("Berhasil Ubah Password")
.setMessage(message)
.setPositiveButton("OK") { _, _ ->
// Navigate back to login activity
finish()
}
.setCancelable(false)
.show()
}
private fun handleError(errorMessage: String) {
Toast.makeText(this, "Error: $errorMessage", Toast.LENGTH_LONG).show()
// Optionally show error dialog
AlertDialog.Builder(this)
.setTitle("Gagal Ubah Password")
.setMessage(errorMessage)
.setPositiveButton("OK", null)
.show()
}
}

View File

@ -204,6 +204,13 @@ class RegisterStep1Fragment : Fragment() {
} }
} }
} }
registerViewModel.toastMessage.observe(viewLifecycleOwner){ event ->
//memanggil toast check value email dan phone
event.getContentIfNotHandled()?.let { msg ->
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
}
}
} }
private fun validateAndProceed() { private fun validateAndProceed() {

View File

@ -88,7 +88,7 @@ class RegisterStep2Fragment : Fragment() {
// Update the email sent message // Update the email sent message
userData?.let { userData?.let {
binding.tvEmailSent.text = "We've sent a verification code to ${it.email}" binding.tvEmailSent.text = "Kami telah mengirimkan kode OTP ke alamat email ${it.email}. Silahkan periksa email anda."
} }
// Start the resend cooldown timer // Start the resend cooldown timer
@ -119,7 +119,7 @@ class RegisterStep2Fragment : Fragment() {
Log.d(TAG, "verifyOtp called with OTP: $otp") Log.d(TAG, "verifyOtp called with OTP: $otp")
if (otp.isEmpty()) { if (otp.isEmpty()) {
Toast.makeText(requireContext(), "Please enter the verification code", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Masukkan kode OTP anda", Toast.LENGTH_SHORT).show()
return return
} }
@ -153,13 +153,13 @@ class RegisterStep2Fragment : Fragment() {
} }
is com.alya.ecommerce_serang.data.repository.Result.Success -> { is com.alya.ecommerce_serang.data.repository.Result.Success -> {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
Toast.makeText(requireContext(), "Verification code resent", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Kode OTP sudah dikirim", Toast.LENGTH_SHORT).show()
startResendCooldown() startResendCooldown()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "OTP request: Error - ${result.exception.message}") Log.e(TAG, "OTP request: Error - ${result.exception.message}")
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
Toast.makeText(requireContext(), "Failed to resend code: ${result.exception.message}", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Gagal mengirim kode OTP", Toast.LENGTH_SHORT).show()
} }
else -> { else -> {
Log.d(TAG, "OTP request: Unknown state") Log.d(TAG, "OTP request: Unknown state")
@ -180,7 +180,7 @@ class RegisterStep2Fragment : Fragment() {
countDownTimer = object : CountDownTimer(30000, 1000) { countDownTimer = object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
timeRemaining = (millisUntilFinished / 1000).toInt() timeRemaining = (millisUntilFinished / 1000).toInt()
binding.tvTimer.text = "Resend available in 00:${String.format("%02d", timeRemaining)}" binding.tvTimer.text = "Kirim ulang OTP dalam waktu 00:${String.format("%02d", timeRemaining)}"
if (timeRemaining % 5 == 0) { if (timeRemaining % 5 == 0) {
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds") Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
} }
@ -188,7 +188,7 @@ class RegisterStep2Fragment : Fragment() {
override fun onFinish() { override fun onFinish() {
Log.d(TAG, "Cooldown finished, enabling resend button") Log.d(TAG, "Cooldown finished, enabling resend button")
binding.tvTimer.text = "You can now resend the code" binding.tvTimer.text = "Dapat mengirim ulang kode OTP"
binding.tvResendOtp.isEnabled = true binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1)) binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0 timeRemaining = 0
@ -222,7 +222,8 @@ class RegisterStep2Fragment : Fragment() {
binding.btnVerify.isEnabled = true binding.btnVerify.isEnabled = true
// Show error message // Show error message
Toast.makeText(requireContext(), "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() Log.e(TAG, "Registration Failed: ${result.exception.message}")
Toast.makeText(requireContext(), "Gagal melakukan regsitrasi", Toast.LENGTH_SHORT).show()
} }
else -> { else -> {
Log.d(TAG, "Registration: Unknown state") Log.d(TAG, "Registration: Unknown state")
@ -251,15 +252,10 @@ class RegisterStep2Fragment : Fragment() {
sessionManager.saveToken(accessToken) sessionManager.saveToken(accessToken)
Log.d(TAG, "Token saved to SessionManager: $accessToken") Log.d(TAG, "Token saved to SessionManager: $accessToken")
// Also save user ID if available in the login response
// result.data.?.let { userId ->
// sessionManager.saveUserId(userId)
// }
Log.d(TAG, "Login successful, token saved: $accessToken")
// Proceed to Step 3 // Proceed to Step 3
Log.d(TAG, "Proceeding to Step 3 after successful login") Log.d(TAG, "Proceeding to Step 3 after successful login")
// call navigate register step from activity
(activity as? RegisterActivity)?.navigateToStep(3, null ) (activity as? RegisterActivity)?.navigateToStep(3, null )
} }
is Result.Error -> { is Result.Error -> {
@ -269,7 +265,7 @@ class RegisterStep2Fragment : Fragment() {
// Show error message but continue to Step 3 anyway // Show error message but continue to Step 3 anyway
Log.e(TAG, "Login failed but proceeding to Step 3", result.exception) Log.e(TAG, "Login failed but proceeding to Step 3", result.exception)
Toast.makeText(requireContext(), "Note: Auto-login failed, but registration was successful", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Berhasil membuat akun, namun belum login", Toast.LENGTH_SHORT).show()
// Proceed to Step 3 // Proceed to Step 3
(activity as? RegisterActivity)?.navigateToStep(3, null) (activity as? RegisterActivity)?.navigateToStep(3, null)

View File

@ -23,11 +23,14 @@ import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterActivity import com.alya.ecommerce_serang.ui.auth.RegisterActivity
import com.alya.ecommerce_serang.ui.order.address.CityAdapter import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
import com.alya.ecommerce_serang.ui.order.address.ViewState import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.ui.order.address.VillagesAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.gson.Gson
class RegisterStep3Fragment : Fragment() { class RegisterStep3Fragment : Fragment() {
private var _binding: FragmentRegisterStep3Binding? = null private var _binding: FragmentRegisterStep3Binding? = null
@ -49,6 +52,8 @@ class RegisterStep3Fragment : Fragment() {
// For province and city selection // For province and city selection
private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) } private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) }
private val cityAdapter by lazy { CityAdapter(requireContext()) } private val cityAdapter by lazy { CityAdapter(requireContext()) }
private val subdistrictAdapter by lazy { SubdsitrictAdapter(requireContext()) }
private val villagesAdapter by lazy { VillagesAdapter(requireContext()) }
companion object { companion object {
private const val TAG = "RegisterStep3Fragment" private const val TAG = "RegisterStep3Fragment"
@ -114,7 +119,7 @@ class RegisterStep3Fragment : Fragment() {
// Observe address submission state // Observe address submission state
observeAddressSubmissionState() observeAddressSubmissionState()
// Load provinces // Load provinces from raja ongkir
Log.d(TAG, "Requesting provinces data") Log.d(TAG, "Requesting provinces data")
registerViewModel.getProvinces() registerViewModel.getProvinces()
setupProvinceObserver() setupProvinceObserver()
@ -171,9 +176,10 @@ class RegisterStep3Fragment : Fragment() {
} }
private fun setupAutoComplete() { private fun setupAutoComplete() {
// Same implementation as before
binding.autoCompleteProvinsi.setAdapter(provinceAdapter) binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter) binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villagesAdapter)
binding.autoCompleteProvinsi.setOnClickListener { binding.autoCompleteProvinsi.setOnClickListener {
binding.autoCompleteProvinsi.showDropDown() binding.autoCompleteProvinsi.showDropDown()
@ -188,6 +194,24 @@ class RegisterStep3Fragment : Fragment() {
} }
} }
binding.autoCompleteKecamatan.setOnClickListener {
if (subdistrictAdapter.count > 0) {
Log.d(TAG, "Subdistrict dropdown clicked, showing ${subdistrictAdapter.count} cities")
binding.autoCompleteKecamatan.showDropDown()
} else {
Toast.makeText(requireContext(), "Pilih kabupaten / kota terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.autoCompleteDesa.setOnClickListener {
if (villagesAdapter.count > 0) {
Log.d(TAG, "Village dropdown clicked, showing ${villagesAdapter.count} cities")
binding.autoCompleteDesa.showDropDown()
} else {
Toast.makeText(requireContext(), "Pilih kecamatan terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ -> binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position) val provinceId = provinceAdapter.getProvinceId(position)
Log.d(TAG, "Province selected at position $position, ID: $provinceId") Log.d(TAG, "Province selected at position $position, ID: $provinceId")
@ -206,13 +230,50 @@ class RegisterStep3Fragment : Fragment() {
cityId?.let { id -> cityId?.let { id ->
Log.d(TAG, "Selected city ID set to: $id") Log.d(TAG, "Selected city ID set to: $id")
registerViewModel.selectedCityId = id registerViewModel.updateSelectedCityId(id)
registerViewModel.getSubdistrict(id)
binding.autoCompleteKecamatan.text.clear()
} }
} }
binding.autoCompleteKecamatan.setOnItemClickListener{ _, _, position, _ ->
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
val subdistictName = subdistrictAdapter.getSubdistrictName(position)
Log.d(TAG, "Subdistrict selected at position $position, ID: $subdistrictId, name: $subdistictName")
subdistrictId?.let { id ->
Log.d(TAG, "Selected subdistrict ID set to: $id")
registerViewModel.selectedSubdistrict = id
registerViewModel.getVillages(id)
binding.autoCompleteDesa.text.clear()
}
subdistictName?.let { name ->
Log.d(TAG, "Selected name subdistrict set to: $name")
registerViewModel.subdistrictName = name
}
}
binding.autoCompleteDesa.setOnItemClickListener{ _, _, position, _ ->
val villageId = villagesAdapter.getVillageId(position)
// val postalCode = villagesAdapter.getPostalCode(position)
Log.d(TAG, "Village selected at position $position, ID: $villageId")
villageId?.let { id ->
Log.d(TAG, "Selected village ID set to: $id")
registerViewModel.selectedVillages = id
}
// postalCode?.let { postCode ->
// registerViewModel.selectedPostalCode = postCode
// }
// binding.etKodePos.setText(registerViewModel.selectedPostalCode ?: "")
}
} }
private fun setupProvinceObserver() { private fun setupProvinceObserver() {
// Same implementation as before // pake raja ongkir
registerViewModel.provincesState.observe(viewLifecycleOwner) { state -> registerViewModel.provincesState.observe(viewLifecycleOwner) { state ->
when (state) { when (state) {
is ViewState.Loading -> { is ViewState.Loading -> {
@ -256,8 +317,46 @@ class RegisterStep3Fragment : Fragment() {
} }
} }
} }
registerViewModel.subdistrictState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBarKecamatan.visibility = View.VISIBLE
}
is ViewState.Success -> {
Log.d(TAG, "Subdistrict: Success - received ${state.data.size} kecamatan")
binding.progressBarKecamatan.visibility = View.GONE
subdistrictAdapter.updateData(state.data)
Log.d(TAG, "Updated subdistrict adapter with ${state.data.size} items")
}
is ViewState.Error -> {
Log.e(TAG, "Subdistrict: Error - ${state.message}")
binding.progressBarKecamatan.visibility = View.GONE
showError("Failed to load kecamatan: ${state.message}")
}
}
}
registerViewModel.villagesState.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBarDesa.visibility = View.VISIBLE
}
is ViewState.Success -> {
Log.d(TAG, "Village: Success - received ${state.data.size} desa")
binding.progressBarDesa.visibility = View.GONE
villagesAdapter.updateData(state.data)
Log.d(TAG, "Updated village adapter with ${state.data.size} items")
}
is ViewState.Error -> {
Log.e(TAG, "Village: Error - ${state.message}")
binding.progressBarDesa.visibility = View.GONE
showError("Failed to load desa: ${state.message}")
}
}
}
} }
private fun submitAddress() { private fun submitAddress() {
Log.d(TAG, "submitAddress called") Log.d(TAG, "submitAddress called")
if (!validateAddressForm()) { if (!validateAddressForm()) {
@ -276,13 +375,16 @@ class RegisterStep3Fragment : Fragment() {
Log.d(TAG, "Using user ID: $userId") Log.d(TAG, "Using user ID: $userId")
val street = binding.etDetailAlamat.text.toString().trim() val street = binding.etDetailAlamat.text.toString().trim()
val subDistrict = binding.etKecamatan.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim() val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim() val phone = binding.etNomorHp.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0 val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
val cityId = registerViewModel.selectedCityId?.toInt() ?: 0 val cityId = registerViewModel.selectedCityId.toString()
val subDistrict = registerViewModel.subdistrictName.toString()
// val postalCode = registerViewModel.selectedPostalCode.toString()
val villageId = registerViewModel.selectedVillages ?: ""
Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode") Log.d(TAG, "Address data - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone") Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
@ -291,21 +393,25 @@ class RegisterStep3Fragment : Fragment() {
// Create address request // Create address request
val addressRequest = CreateAddressRequest( val addressRequest = CreateAddressRequest(
userId = user.id, // must match the type expected in the DB
lat = defaultLatitude, lat = defaultLatitude,
long = defaultLongitude, long = defaultLongitude,
street = street, street = street,
subDistrict = subDistrict, subDistrict = subDistrict,
cityId = cityId, cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId, provId = provinceId,
postCode = postalCode, postCode = postalCode,
idVillage = villageId, // Or provide a real ID if needed
detailAddress = street, detailAddress = street,
userId = userId, isStoreLocation = false,
recipient = recipient, recipient = recipient,
phone = phone, phone = phone
isStoreLocation = false
) )
Log.d(TAG, "Address request created: $addressRequest") Log.d(TAG, "Address request created: $addressRequest")
val gson = Gson()
val jsonString = gson.toJson(addressRequest)
Log.d(TAG, "Request JSON: $jsonString")
// Show loading // Show loading
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@ -318,13 +424,13 @@ class RegisterStep3Fragment : Fragment() {
private fun validateAddressForm(): Boolean { private fun validateAddressForm(): Boolean {
val street = binding.etDetailAlamat.text.toString().trim() val street = binding.etDetailAlamat.text.toString().trim()
val subDistrict = binding.etKecamatan.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim() val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim() val phone = binding.etNomorHp.text.toString().trim()
val provinceId = registerViewModel.selectedProvinceId val provinceId = registerViewModel.selectedProvinceId
val cityId = registerViewModel.selectedCityId val cityId = registerViewModel.selectedCityId
val subDistrict = registerViewModel.selectedSubdistrict
val postalCode = registerViewModel.selectedPostalCode
Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode") Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone") Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone")
@ -362,6 +468,12 @@ class RegisterStep3Fragment : Fragment() {
return false return false
} }
if (subDistrict == null) {
showError("Pilih kota/kabupaten terlebih dahulu")
binding.autoCompleteKecamatan.requestFocus()
return false
}
return true return true
} }
@ -409,8 +521,4 @@ class RegisterStep3Fragment : Fragment() {
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null) ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
_binding = null _binding = null
} }
//
// // Data classes for province and city
// data class Province(val id: String, val name: String)
// data class City(val id: String, val name: String)
} }

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.cart package com.alya.ecommerce_serang.ui.cart
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -29,6 +30,8 @@ class CartActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private lateinit var storeAdapter: StoreAdapter private lateinit var storeAdapter: StoreAdapter
private var TAG = "Cart Activity"
private val viewModel: CartViewModel by viewModels { private val viewModel: CartViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -134,18 +137,52 @@ class CartActivity : AppCompatActivity() {
} }
private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) { private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) {
// Extract cart item IDs and wholesale status val wholesalePriceMap = viewModel.cartItemWholesalePrice.value ?: emptyMap()
val cartItemIds = checkoutItems.map { it.cartItem.cartItemId }
val wholesaleArray = checkoutItems.map { it.isWholesale }.toBooleanArray()
// Start checkout activity with the cart items and wholesale info val updatedItems = checkoutItems.map { info ->
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray) val wholesalePrice = wholesalePriceMap[info.cartItem.cartItemId]
val updatedCartItem = if (info.isWholesale && wholesalePrice != null) {
// Replace the price with wholesale price
info.cartItem.copy(price = wholesalePrice.toInt())
} else {
info.cartItem
}
// Debug log
Log.d(
TAG,
"cartItemId: ${updatedCartItem.cartItemId}, " +
"isWholesale: ${info.isWholesale}, " +
"wholesalePrice: $wholesalePrice, " +
"finalPrice: ${updatedCartItem.price}"
)
info.copy(cartItem = updatedCartItem)
}
val cartItemIds = updatedItems.map { it.cartItem.cartItemId }
val wholesaleArray = updatedItems.map { it.isWholesale }.toBooleanArray()
// FIX: Pass wholesale prices as IntArray
val wholesalePricesArray = updatedItems.map { info ->
if (info.isWholesale) {
val wholesalePrice = wholesalePriceMap[info.cartItem.cartItemId]
wholesalePrice?.toInt() ?: info.cartItem.price
} else {
info.cartItem.price
}
}.toIntArray()
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray, wholesalePricesArray)
} }
private fun observeViewModel() { private fun observeViewModel() {
viewModel.cartItems.observe(this) { cartItems -> viewModel.cartItems.observe(this) { cartItems ->
if (cartItems.isNullOrEmpty()) { if (cartItems.isNullOrEmpty()) {
binding.emptyCart.visibility = View.VISIBLE
showEmptyState(true) showEmptyState(true)
} else { } else {
showEmptyState(false) showEmptyState(false)
storeAdapter.submitList(cartItems) storeAdapter.submitList(cartItems)
@ -153,7 +190,8 @@ class CartActivity : AppCompatActivity() {
} }
viewModel.isLoading.observe(this) { isLoading -> viewModel.isLoading.observe(this) { isLoading ->
// Show/hide loading indicator if needed binding.progressBarCart?.visibility = if (isLoading) View.VISIBLE else View.GONE
Log.d("CartActivity", "Loading state: $isLoading")
} }
viewModel.errorMessage.observe(this) { errorMessage -> viewModel.errorMessage.observe(this) { errorMessage ->
@ -229,7 +267,5 @@ class CartActivity : AppCompatActivity() {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(amount).replace("Rp", "Rp ") return format.format(amount).replace("Rp", "Rp ")
} }
} }

View File

@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
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
@ -129,6 +130,7 @@ class ChatActivity : AppCompatActivity() {
return return
} }
// set up data toko
binding.tvStoreName.text = storeName binding.tvStoreName.text = storeName
val fullImageUrl = when (val img = storeImg) { val fullImageUrl = when (val img = storeImg) {
is String -> { is String -> {
@ -142,7 +144,7 @@ class ChatActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.imgProfile) .into(binding.imgProfile)
// Set chat parameters to ViewModel // Set chat parameter to send to ViewModel with product
viewModel.setChatParameters( viewModel.setChatParameters(
storeId = storeId, storeId = storeId,
productId = productId, productId = productId,
@ -159,16 +161,17 @@ class ChatActivity : AppCompatActivity() {
} }
// Setup UI components // Setup UI components
// rv isi chat
setupRecyclerView() setupRecyclerView()
setupWindowInsets() setupWindowInsets()
setupListeners() setupListeners()
setupTypingIndicator() setupTypingIndicator()
// observe listener from viewmodel
observeViewModel() observeViewModel()
// If opened from ChatListFragment with a valid chatRoomId // If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) { if (chatRoomId > 0) {
// Directly set the chatRoomId and load chat history viewModel.setChatRoomId(chatRoomId)
viewModel._chatRoomId.value = chatRoomId
} }
} }
@ -402,68 +405,71 @@ class ChatActivity : AppCompatActivity() {
} }
}) })
viewModel.state.observe(this, Observer { state -> lifecycleScope.launchWhenStarted {
Log.d(TAG, "State updated - Messages: ${state.messages.size}") viewModel.state.collect() { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages // Update messages
val previousCount = chatAdapter.itemCount val previousCount = chatAdapter.itemCount
val displayItems = viewModel.getDisplayItems() val displayItems = viewModel.getDisplayItems()
chatAdapter.submitList(displayItems) { chatAdapter.submitList(displayItems) {
Log.d(TAG, "Messages submitted to adapter") Log.d(TAG, "Messages submitted to adapter")
// Only auto-scroll for new messages or initial load // Only auto-scroll for new messages or initial load
if (previousCount == 0 || state.messages.size > previousCount) { if (previousCount == 0 || state.messages.size > previousCount) {
scrollToBottomInstant() scrollToBottomInstant()
}
}
// layout attach product
if (!state.productName.isNullOrEmpty()) {
binding.tvProductName.text = state.productName
binding.tvProductPrice.text = state.productPrice
binding.ratingBar.rating = state.productRating
binding.tvRating.text = state.productRating.toString()
binding.tvSellerName.text = state.storeName
binding.tvStoreName.text = state.storeName
val fullImageUrl = when (val img = state.productImageUrl) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
} }
else -> R.drawable.placeholder_image
} }
if (!state.productImageUrl.isNullOrEmpty()) { // layout attach product
Glide.with(this@ChatActivity) if (!state.productName.isNullOrEmpty()) {
.load(fullImageUrl) binding.tvProductName.text = state.productName
.centerCrop() binding.tvProductPrice.text = state.productPrice
.placeholder(R.drawable.placeholder_image) binding.ratingBar.rating = state.productRating
.error(R.drawable.placeholder_image) binding.tvRating.text = state.productRating.toString()
.into(binding.imgProduct) binding.tvSellerName.text = state.storeName
binding.tvStoreName.text = state.storeName
val fullImageUrl = when (val img = state.productImageUrl) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image
}
if (!state.productImageUrl.isNullOrEmpty()) {
Glide.with(this@ChatActivity)
.load(fullImageUrl)
.centerCrop()
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(binding.imgProduct)
}
updateProductCardUI(state.hasProductAttachment)
binding.productContainer.visibility = View.GONE
} else {
binding.productContainer.visibility = View.GONE
} }
updateProductCardUI(state.hasProductAttachment)
binding.productContainer.visibility = View.GONE updateInputHint(state)
} else {
binding.productContainer.visibility = View.GONE // Update attachment hint
if (state.hasAttachment) {
binding.layoutAttachImage.visibility = View.VISIBLE
} else {
binding.editTextMessage.hint = getString(R.string.write_message)
}
// Show error if any
state.error?.let { error ->
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
} }
}
updateInputHint(state)
// Update attachment hint
if (state.hasAttachment) {
binding.layoutAttachImage.visibility = View.VISIBLE
} else {
binding.editTextMessage.hint = getString(R.string.write_message)
}
// Show error if any
state.error?.let { error ->
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
})
} }
private fun updateInputHint(state: ChatUiState) { private fun updateInputHint(state: ChatUiState) {
@ -489,7 +495,7 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
// You can navigate to product detail here // You can navigate to product detail here
navigateToProductDetail(productInfo.productId) navigateToProductDetail(productInfo.productId)
} }
private fun navigateToProductDetail(productId: Int) { private fun navigateToProductDetail(productId: Int) {

View File

@ -209,7 +209,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice binding.tvProductPrice.text = product.productPrice
// Load product image // Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) { val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1) BASE_URL + product.productImage.substring(1)
} else { } else {
product.productImage product.productImage
@ -246,7 +246,7 @@ class ChatAdapter(
binding.tvProductPrice.text = product.productPrice binding.tvProductPrice.text = product.productPrice
// Load product image // Load product image
val fullImageUrl = if (product.productImage.startsWith("/")) { val fullImageUrl = if (product.productImage!!.startsWith("/")) {
BASE_URL + product.productImage.substring(1) BASE_URL + product.productImage.substring(1)
} else { } else {
product.productImage product.productImage

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.chat package com.alya.ecommerce_serang.ui.chat
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -55,31 +56,44 @@ class ChatListFragment : Fragment() {
viewModel.chatList.observe(viewLifecycleOwner) { result -> viewModel.chatList.observe(viewLifecycleOwner) { result ->
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
val adapter = ChatListAdapter(result.data) { chatItem -> val data = result.data
// Use the ChatActivity.createIntent factory method for proper navigation
ChatActivity.createIntent( binding.tvEmptyChat.visibility = View.GONE
context = requireActivity(), if (data.isNotEmpty()) {
storeId = chatItem.storeId, val adapter = ChatListAdapter(data) { chatItem ->
productId = 0, // Default value since we don't have it in ChatListItem ChatActivity.createIntent(
productName = null, // Null is acceptable as per ChatActivity context = requireActivity(),
productPrice = "", storeId = chatItem.storeId,
productImage = null, productId = 0,
productRating = null, productName = null,
storeName = chatItem.storeName, productPrice = "",
chatRoomId = chatItem.chatRoomId, productImage = null,
storeImage = chatItem.storeImage productRating = null,
) storeName = chatItem.storeName,
chatRoomId = chatItem.chatRoomId,
storeImage = chatItem.storeImage
)
}
binding.chatListRecyclerView.adapter = adapter
} else {
binding.tvEmptyChat.visibility = View.VISIBLE
} }
binding.chatListRecyclerView.adapter = adapter
} }
is Result.Error -> { is Result.Error -> {
binding.tvEmptyChat.visibility = View.VISIBLE
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
} }
Result.Loading -> { Result.Loading -> {
binding.progressBarChat.visibility = View.VISIBLE
// Optional: show progress bar // Optional: show progress bar
} }
} }
} }
//loading chat list
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
binding.progressBarChat?.visibility = if (isLoading) View.VISIBLE else View.GONE
Log.d(TAG, "Loading state: $isLoading")
}
} }
@ -89,6 +103,6 @@ class ChatListFragment : Fragment() {
} }
companion object{ companion object{
private var TAG = "ChatListFragment"
} }
} }

View File

@ -14,6 +14,9 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.utils.Constants import com.alya.ecommerce_serang.utils.Constants
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -56,9 +59,12 @@ class ChatViewModel @Inject constructor(
// Product attachment flag // Product attachment flag
private var shouldAttachProduct = false private var shouldAttachProduct = false
// UI state using LiveData // use state for more seamless responsive
private val _state = MutableLiveData(ChatUiState()) private val _state = MutableStateFlow(ChatUiState())
val state: LiveData<ChatUiState> = _state val state: StateFlow<ChatUiState> = _state
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
val _chatRoomId = MutableLiveData<Int>(0) val _chatRoomId = MutableLiveData<Int>(0)
val chatRoomId: LiveData<Int> = _chatRoomId val chatRoomId: LiveData<Int> = _chatRoomId
@ -90,16 +96,21 @@ class ChatViewModel @Inject constructor(
init { init {
Log.d(TAG, "ChatViewModel initialized") Log.d(TAG, "ChatViewModel initialized")
socketService.connect() // 🛠 force connection
setupSocketListeners() // 🛠 always listen, even before user data
initializeUser() initializeUser()
} }
private fun initializeUser() { private fun initializeUser() {
_isLoading.value = true
viewModelScope.launch { viewModelScope.launch {
Log.d(TAG, "Initializing user session...") Log.d(TAG, "Initializing user session...")
when (val result = chatRepository.fetchUserProfile()) { when (val result = chatRepository.fetchUserProfile()) {
is Result.Success -> { is Result.Success -> {
currentUserId = result.data?.userId currentUserId = result.data?.userId
_isLoading.value = false
Log.d(TAG, "User session initialized - User ID: $currentUserId") Log.d(TAG, "User session initialized - User ID: $currentUserId")
if (currentUserId == null || currentUserId == 0) { if (currentUserId == null || currentUserId == 0) {
@ -107,14 +118,17 @@ class ChatViewModel @Inject constructor(
updateState { it.copy(error = "User authentication error. Please login again.") } updateState { it.copy(error = "User authentication error. Please login again.") }
} else { } else {
Log.d(TAG, "Setting up socket listeners...") Log.d(TAG, "Setting up socket listeners...")
socketService.connect()
setupSocketListeners() setupSocketListeners()
} }
} }
is Result.Error -> { is Result.Error -> {
_isLoading.value = false
Log.e(TAG, "Failed to fetch user profile: ${result.exception.message}") Log.e(TAG, "Failed to fetch user profile: ${result.exception.message}")
updateState { it.copy(error = "User authentication error. Please login again.") } updateState { it.copy(error = "User authentication error. Please login again.") }
} }
is Result.Loading -> { is Result.Loading -> {
_isLoading.value = true
Log.d(TAG, "Loading user profile...") Log.d(TAG, "Loading user profile...")
} }
} }
@ -223,26 +237,116 @@ class ChatViewModel @Inject constructor(
if (connectionState is ConnectionState.Connected) { if (connectionState is ConnectionState.Connected) {
Log.d(TAG, "Socket connected, joining room...") Log.d(TAG, "Socket connected, joining room...")
socketService.joinRoom() val roomId = _chatRoomId.value
if (roomId != null && roomId > 0) {
socketService.joinRoom(roomId)
}
} }
} }
} }
// viewModelScope.launch {
// socketService.newMessages.collect { chatLine ->
// chatLine?.let {
// Log.d(TAG, "NEW message received in ViewModel: ${it.message}")
// val updatedMessages = _state.value.messages.toMutableList()
// updatedMessages.add(convertChatLineToUiMessage(it))
// updateState { it.copy(messages = updatedMessages) }
//
// if (it.senderId != currentUserId) {
// updateMessageStatus(it.id, Constants.STATUS_READ)
// }
// }
// }
// }
viewModelScope.launch { viewModelScope.launch {
socketService.newMessages.collect { chatLine -> socketService.newMessages.collect { chatLine ->
chatLine?.let { Log.d("ChatViewModel", "Collected new message from SocketIOService: ${chatLine.message}")
Log.d(TAG, "New message received via socket - ID: ${it.id}, SenderID: ${it.senderId}") chatLine?.let { incomingChatLine ->
val currentMessages = _state.value?.messages ?: listOf() // 1. First update: Add the message to the list (potentially without full product info)
val updatedMessages = currentMessages.toMutableList().apply { _state.update { currentState ->
add(convertChatLineToUiMessage(it)) val existingMessageIndex =
currentState.messages.indexOfFirst { it.id == incomingChatLine.id }
val messagesAfterInitialUpdate = if (existingMessageIndex != -1) {
// If message exists (e.g., status update), just update it
val updatedList = currentState.messages.toMutableList()
updatedList[existingMessageIndex] = mapChatLineToUiMessage(
incomingChatLine,
updatedList[existingMessageIndex].productInfo
) // Preserve existing productInfo if any
updatedList
} else {
// New message, add it
(currentState.messages + mapChatLineToUiMessage(incomingChatLine)).distinctBy { msg -> msg.id }
}
// Sort after any update/addition
currentState.copy(messages = messagesAfterInitialUpdate.sortedBy { msg ->
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).parse(msg.createdAt)?.time
})
} }
updateState { it.copy(messages = updatedMessages) }
if (it.senderId != currentUserId) { // 2. If it's a product message and needs details, fetch them
Log.d(TAG, "Marking message as read: ${it.id}") if (incomingChatLine.productId != 0) { // Check if it's a product message
updateMessageStatus(it.id, Constants.STATUS_READ) viewModelScope.launch {
Log.d(
TAG,
"Fetching product detail for ID: ${incomingChatLine.productId}"
)
// Call your repository function directly
val productResponse =
chatRepository.fetchProductDetail(incomingChatLine.productId)
if (productResponse != null && productResponse.product != null) {
val fetchedProduct =
productResponse.product // Access the nested product object
Log.d(
TAG,
"Successfully fetched product: ${fetchedProduct.productName}"
)
// Create a complete ProductInfo object
val fullProductInfo = ProductInfo(
productId = fetchedProduct.productId,
productName = fetchedProduct.productName, // Use productName from fetched data
productPrice = fetchedProduct.price, // Use productPrice from fetched data
productImage = fetchedProduct.image, // Use productImage from fetched data
productRating = fetchedProduct.rating.toFloat(),
storeName = fetchedProduct.productName // Use storeName from fetched data
)
// --- PHASE 3: Second UI update (fill in full product info) ---
_state.update { currentState ->
val updatedMessages = currentState.messages.map { msg ->
if (msg.id == incomingChatLine.id) {
// Found the message, update its productInfo with full details
msg.copy(productInfo = fullProductInfo)
} else {
msg
}
}
currentState.copy(messages = updatedMessages)
}
} else {
Log.e(
TAG,
"Failed to fetch product detail for ID ${incomingChatLine.productId} or product data is null."
)
// Optionally, update message status to indicate error in product loading
}
}
} }
} }
// // Your existing logic for clearing typing status etc.
// if (incomingChatLine.isTyping == false && incomingChatLine.from?.id != sessionManager.getUserId()?.toIntOrNull()) {
// _state.update { it.copy(isOtherUserTyping = false) }
// }
} }
} }
@ -263,10 +367,10 @@ class ChatViewModel @Inject constructor(
if (roomId <= 0) { if (roomId <= 0) {
Log.e(TAG, "Cannot join room: Invalid room ID") Log.e(TAG, "Cannot join room: Invalid room ID")
return return
} else if (roomId > 0){
Log.d(TAG, "Joining socket room: $roomId")
socketService.joinRoom(roomId)
} }
Log.d(TAG, "Joining socket room: $roomId")
socketService.joinRoom()
} }
fun sendTypingStatus(isTyping: Boolean) { fun sendTypingStatus(isTyping: Boolean) {
@ -335,10 +439,13 @@ class ChatViewModel @Inject constructor(
} }
fun getChatList() { fun getChatList() {
_isLoading.value = true
Log.d(TAG, "Getting chat list...") Log.d(TAG, "Getting chat list...")
viewModelScope.launch { viewModelScope.launch {
_chatList.value = Result.Loading // _chatList.value = Result.Loading
_chatList.value = chatRepository.getListChat() _chatList.value = chatRepository.getListChat()
_isLoading.value = false
} }
} }
@ -717,7 +824,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//update message status //update message status
fun updateMessageStatus(messageId: Int, status: String) { fun updateMessageStatus(messageId: Int, status: String) {
Log.d(TAG, "Updating message status - ID: $messageId, Status: $status") Log.d(TAG, "Updating message status - ID: $messageId, Status: $status")
@ -745,7 +852,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//set image attachment //set image attachment
fun setSelectedImageFile(file: File?) { fun setSelectedImageFile(file: File?) {
selectedImageFile = file selectedImageFile = file
updateState { it.copy(hasAttachment = file != null) } updateState { it.copy(hasAttachment = file != null) }
@ -780,7 +887,7 @@ class ChatViewModel @Inject constructor(
) )
} }
// convert chat history item to ui // convert chat history item to ui
private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage { private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
val formattedTime = formatTimestamp(chatItem.createdAt) val formattedTime = formatTimestamp(chatItem.createdAt)
@ -921,7 +1028,7 @@ class ChatViewModel @Inject constructor(
} }
} }
//format price //format price
private fun formatPrice(price: String): String { private fun formatPrice(price: String): String {
return if (price.startsWith("Rp")) price else "Rp$price" return if (price.startsWith("Rp")) price else "Rp$price"
} }
@ -947,9 +1054,7 @@ class ChatViewModel @Inject constructor(
// helper function to update live data // helper function to update live data
private fun updateState(update: (ChatUiState) -> ChatUiState) { private fun updateState(update: (ChatUiState) -> ChatUiState) {
_state.value?.let { _state.value = update(_state.value)
_state.value = update(it)
}
} }
//clear any error messages //clear any error messages
@ -1042,6 +1147,73 @@ class ChatViewModel @Inject constructor(
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean { private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR) return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
} }
fun setChatRoomId(roomId: Int) {
_chatRoomId.value = roomId
joinSocketRoom(roomId)
loadChatHistory(roomId)
}
private fun convertToUiMessage(chatLine: ChatLine): ChatUiMessage {
val formattedTime = formatTimestamp(chatLine.createdAt)
return ChatUiMessage(
id = chatLine.id,
message = chatLine.message,
attachment = chatLine.attachment,
status = chatLine.status,
time = formattedTime, // or format from createdAt if needed
isSentByMe = chatLine.senderId == currentUserId,
messageType = MessageType.TEXT, // or detect from chatLine if needed
productInfo = null, // optional, if applicable
createdAt = chatLine.createdAt
)
}
private fun mapChatLineToUiMessage(chatLine: ChatLine, fetchedProductInfo: ProductInfo? = null): ChatUiMessage {
val isSentByMe = chatLine.senderId == sessionManager.getUserId()?.toIntOrNull() // Using senderId now
val formattedTime = try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val date = inputFormat.parse(chatLine.createdAt)
date?.let { outputFormat.format(it) } ?: ""
} catch (e: Exception) {
Log.e("ChatViewModel", "Error parsing date: ${chatLine.createdAt}", e)
""
}
// Determine message type based on what ChatLine provides
val messageType = when {
chatLine.attachment?.isNotEmpty() == true -> MessageType.IMAGE
chatLine.productId != 0 -> MessageType.PRODUCT // If productId is non-zero, it's a product message
else -> MessageType.TEXT
}
// Initialize productInfo: if fetchedProductInfo is provided, use it.
// Otherwise, if ChatLine has a productId, create a ProductInfo with just the ID.
// If no productId, it's null.
val productInfo = fetchedProductInfo ?: if (chatLine.productId != 0) {
// Create a placeholder ProductInfo with just the ID for initial display
// The full details will be fetched later
ProductInfo(productId = chatLine.productId)
} else {
null
}
return ChatUiMessage(
id = chatLine.id,
message = chatLine.message,
attachment = chatLine.attachment,
status = chatLine.status,
time = formattedTime,
isSentByMe = isSentByMe,
messageType = messageType,
productInfo = productInfo, // Use the determined productInfo
createdAt = chatLine.createdAt
)
}
} }
enum class MessageType { enum class MessageType {
@ -1051,12 +1223,12 @@ enum class MessageType {
} }
data class ProductInfo( data class ProductInfo(
val productId: Int, val productId: Int, // Keep productId here
val productName: String, val productName: String? = null, // Make nullable
val productPrice: String, val productPrice: String? = null, // Make nullable
val productImage: String, val productImage: String? = null, // Make nullable
val productRating: Float, val productRating: Float = 0f, // Default value
val storeName: String val storeName: String? = null
) )
// representing chat messages to UI // representing chat messages to UI
@ -1072,8 +1244,6 @@ data class ChatUiMessage(
val createdAt: String val createdAt: String
) )
// representing UI state to screen // representing UI state to screen
data class ChatUiState( data class ChatUiState(
val messages: List<ChatUiMessage> = emptyList(), val messages: List<ChatUiMessage> = emptyList(),
@ -1091,4 +1261,8 @@ data class ChatUiState(
val productImageUrl: String = "", val productImageUrl: String = "",
val productRating: Float = 0f, val productRating: Float = 0f,
val storeName: String = "" val storeName: String = ""
) )
//data class ChatUiState(
// val messages: List<ChatUiMessage> = emptyList()
//)

View File

@ -10,14 +10,24 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.google.gson.Gson import com.google.gson.Gson
import io.socket.client.IO import io.socket.client.IO
import io.socket.client.Socket import io.socket.client.Socket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.net.URISyntaxException import java.net.URISyntaxException
import javax.inject.Inject
import javax.inject.Singleton
class SocketIOService( @Singleton
class SocketIOService @Inject constructor(
private val sessionManager: SessionManager private val sessionManager: SessionManager
) { ) {
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val TAG = "SocketIOService" private val TAG = "SocketIOService"
// Socket.IO client // Socket.IO client
@ -30,8 +40,8 @@ class SocketIOService(
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected()) private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected())
val connectionState: StateFlow<ConnectionState> = _connectionState val connectionState: StateFlow<ConnectionState> = _connectionState
private val _newMessages = MutableStateFlow<ChatLine?>(null) private val _newMessages = MutableSharedFlow<ChatLine>(extraBufferCapacity = 1) // Using extraBufferCapacity for a non-suspending emit
val newMessages: StateFlow<ChatLine?> = _newMessages val newMessages: SharedFlow<ChatLine> = _newMessages
private val _typingStatus = MutableStateFlow<TypingStatus?>(null) private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
val typingStatus: StateFlow<TypingStatus?> = _typingStatus val typingStatus: StateFlow<TypingStatus?> = _typingStatus
@ -85,63 +95,95 @@ class SocketIOService(
* Sets up Socket.IO event listeners * Sets up Socket.IO event listeners
*/ */
private fun setupSocketListeners() { private fun setupSocketListeners() {
socket?.let { socket ->
// Connection events
socket.on(Socket.EVENT_CONNECT) {
Log.d(TAG, "Socket.IO connected")
isConnected = true
_connectionState.value = ConnectionState.Connected
_connectionStateLiveData.postValue(ConnectionState.Connected)
}
socket.on(Socket.EVENT_DISCONNECT) { socket?.on(Constants.EVENT_NEW_MESSAGE) { args -> // Use the event name your server emits
Log.d(TAG, "Socket.IO disconnected") Log.d(TAG, "Raw event received on ${Constants.EVENT_NEW_MESSAGE}: ${args.firstOrNull()}") // Check raw args
isConnected = false
_connectionState.value = ConnectionState.Disconnected("Disconnected from server")
_connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
}
socket.on(Socket.EVENT_CONNECT_ERROR) { args -> if (args.isNotEmpty()) {
val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
Log.e(TAG, "Socket.IO connection error: $error")
isConnected = false
_connectionState.value = ConnectionState.Error("Connection error: $error")
_connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
}
// Chat events
socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
try { try {
if (args.isNotEmpty() && args[0] != null) { val messageJson = args[0].toString()
val messageJson = args[0].toString() val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
Log.d(TAG, "Received new message: $messageJson") Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java) Log.d(TAG, "Emitting new message to _newMessages SharedFlow...") // New log
_newMessages.value = chatLine
_newMessagesLiveData.postValue(chatLine) // Use the serviceScope to launch a coroutine for emit()
serviceScope.launch {
_newMessages.emit(chatLine) // This ensures every message is processed
Log.d(TAG, "New message emitted to SharedFlow.") // New log after emit
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error parsing new message event", e) Log.e(TAG, "Error parsing or emitting new message: ${e.message}", e)
}
}
socket.on(Constants.EVENT_TYPING) { args ->
try {
if (args.isNotEmpty() && args[0] != null) {
val typingData = args[0] as JSONObject
val userId = typingData.getInt("userId")
val roomId = typingData.getInt("roomId")
val isTyping = typingData.getBoolean("isTyping")
Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
val status = TypingStatus(userId, roomId, isTyping)
_typingStatus.value = status
_typingStatusLiveData.postValue(status)
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing typing event", e)
} }
} else {
Log.w(TAG, "Received empty args for ${Constants.EVENT_NEW_MESSAGE}")
} }
} }
// socket?.on(Constants.EVENT_NEW_MESSAGE) { args ->
// if (args.isNotEmpty()) {
// val messageJson = args[0].toString()
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
// Log.d("SocketIOService", "Message received: ${chatLine.message}")
// _newMessages.value = chatLine
// }
// }
// socket?.let { socket ->
// // Connection events
// socket.on(Socket.EVENT_CONNECT) {
// Log.d(TAG, "Socket.IO connected")
// isConnected = true
// _connectionState.value = ConnectionState.Connected
// _connectionStateLiveData.postValue(ConnectionState.Connected)
// }
//
// socket.on(Socket.EVENT_DISCONNECT) {
// Log.d(TAG, "Socket.IO disconnected")
// isConnected = false
// _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
// _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
// }
//
// socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
// val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
// Log.e(TAG, "Socket.IO connection error: $error")
// isConnected = false
// _connectionState.value = ConnectionState.Error("Connection error: $error")
// _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
// }
//
// // Chat events
// socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
// try {
// if (args.isNotEmpty() && args[0] != null) {
// val messageJson = args[0].toString()
// Log.d(TAG, "Received new message: $messageJson")
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
// _newMessages.value = chatLine
// _newMessagesLiveData.postValue(chatLine)
// }
// } catch (e: Exception) {
// Log.e(TAG, "Error parsing new message event", e)
// }
// }
//
// socket.on(Constants.EVENT_TYPING) { args ->
// try {
// if (args.isNotEmpty() && args[0] != null) {
// val typingData = args[0] as JSONObject
// val userId = typingData.getInt("userId")
// val roomId = typingData.getInt("roomId")
// val isTyping = typingData.getBoolean("isTyping")
//
// Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
// val status = TypingStatus(userId, roomId, isTyping)
// _typingStatus.value = status
// _typingStatusLiveData.postValue(status)
// }
// } catch (e: Exception) {
// Log.e(TAG, "Error parsing typing event", e)
// }
// }
// }
} }
/** /**
@ -159,22 +201,29 @@ class SocketIOService(
/** /**
* Joins a specific chat room * Joins a specific chat room
*/ */
fun joinRoom() { fun joinRoom(roomId: Int) {
// if (!isConnected) {
// connect()
// return
// }
//
// // Get user ID from SessionManager
// val userId = sessionManager.getUserId()
// if (userId.isNullOrEmpty()) {
// Log.e(TAG, "Cannot join room: User ID is null or empty")
// return
// }
//
// // Join the room using the current user's ID
// socket?.emit("joinRoom", roomId) // ✅
// Log.d(TAG, "Joined room ID: $roomId")
// Log.d(TAG, "Joined room for user: $userId")
if (!isConnected) { if (!isConnected) {
connect() connect()
return
} }
// Get user ID from SessionManager socket?.emit("joinRoom", roomId)
val userId = sessionManager.getUserId() Log.d(TAG, "Joined room ID: $roomId")
if (userId.isNullOrEmpty()) {
Log.e(TAG, "Cannot join room: User ID is null or empty")
return
}
// Join the room using the current user's ID
socket?.emit("joinRoom", userId)
Log.d(TAG, "Joined room for user: $userId")
} }
/** /**

View File

@ -208,25 +208,6 @@ class HomeFragment : Fragment() {
private fun initUi() { private fun initUi() {
// For LightStatusBar // For LightStatusBar
setLightStatusBar() setLightStatusBar()
// val banners = binding.banners
// banners.offscreenPageLimit = 1
//
// val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
// val currentItemHorizontalMarginPx =
// resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
// val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
//
// banners.setPageTransformer { page, position ->
// page.translationX = -pageTranslationX * position
// page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
// }
//
// banners.addItemDecoration(
// HorizontalMarginItemDecoration(
// requireContext(),
// R.dimen.viewpager_current_item_horizontal_margin
// )
// )
} }
private fun handleProductClick(product: ProductsItem) { private fun handleProductClick(product: ProductsItem) {
@ -248,8 +229,4 @@ class HomeFragment : Fragment() {
categoryAdapter = null categoryAdapter = null
_binding = null _binding = null
} }
// private fun showLoading(isLoading: Boolean) {
// binding.progressBar.isVisible = isLoading
// }
} }

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.home
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
@ -65,6 +66,16 @@ class SearchResultsAdapter(
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store" val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
binding.tvStoreName.text = storeName binding.tvStoreName.text = storeName
val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull()
if (ratingValue != null && ratingValue > 0f) {
binding.rating.text = String.format("%.1f", ratingValue)
binding.rating.visibility = View.VISIBLE
} else {
binding.rating.text = "Belum ada rating"
binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
}
} }
} }

View File

@ -4,6 +4,7 @@ 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.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
@ -95,26 +96,43 @@ class CheckoutActivity : AppCompatActivity() {
// Process Cart checkout flow // Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList() val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE) val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE)
val wholesalePricesArray = intent.getIntArrayExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES)
if (cartItemIds.isNotEmpty()) { if (cartItemIds.isNotEmpty()) {
// Create a map of cart item IDs to wholesale status if available // Build map of cartItemId -> isWholesale
val wholesaleMap = if (isWholesaleArray != null && isWholesaleArray.size == cartItemIds.size) { val isWholesaleMap = if (isWholesaleArray != null && isWholesaleArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id -> id to isWholesaleArray[index] }.toMap() cartItemIds.mapIndexed { index, id ->
id to isWholesaleArray[index]
}.toMap()
} else { } else {
emptyMap() emptyMap()
} }
viewModel.initializeFromCart(cartItemIds, wholesaleMap) // Build wholesalePriceMap - FIX: Map cartItemId to wholesale price
val wholesalePriceMap = if (wholesalePricesArray != null && wholesalePricesArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id ->
id to wholesalePricesArray[index]
}.toMap()
} else {
emptyMap()
}
viewModel.initializeFromCart(
cartItemIds,
isWholesaleMap,
wholesalePriceMap
)
Log.d("CheckoutActivity", "Cart IDs: $cartItemIds")
Log.d("CheckoutActivity", "IsWholesaleArray: ${isWholesaleArray?.joinToString()}")
Log.d("CheckoutActivity", "WholesalePricesArray: ${wholesalePricesArray?.joinToString()}")
Log.d("CheckoutActivity", "IsWholesaleMap: $isWholesaleMap")
Log.d("CheckoutActivity", "WholesalePriceMap: $wholesalePriceMap")
} else { } else {
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Tidak ada item keranjang", Toast.LENGTH_SHORT).show()
finish() finish()
} }
} }
// viewModel.getPaymentMethods { paymentMethods ->
// // Logging is just for debugging
// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
// }
} }
private fun setupToolbar() { private fun setupToolbar() {
@ -165,7 +183,7 @@ class CheckoutActivity : AppCompatActivity() {
// Observe loading state // Observe loading state
viewModel.isLoading.observe(this) { isLoading -> viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading binding.btnPay.isEnabled = !isLoading
// Show/hide loading indicator if you have one
} }
// Observe error messages // Observe error messages
@ -273,10 +291,14 @@ class CheckoutActivity : AppCompatActivity() {
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) { private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
if (shipName.isNotEmpty() && shipService.isNotEmpty()) { if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
// Display shipping name and service in one line // Display shipping name and service in one line
binding.cardShipment.visibility = View.VISIBLE
binding.tvCourierName.text = "$shipName $shipService" binding.tvCourierName.text = "$shipName $shipService"
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja" binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble()) binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
binding.rbJne.isChecked = true binding.rbJne.isChecked = true
} else {
binding.cardShipment.visibility = View.GONE
} }
} }
@ -416,6 +438,7 @@ class CheckoutActivity : AppCompatActivity() {
const val EXTRA_PRICE = "PRICE" const val EXTRA_PRICE = "PRICE"
const val EXTRA_ISWHOLESALE = "ISWHOLESALE" const val EXTRA_ISWHOLESALE = "ISWHOLESALE"
const val EXTRA_CART_ITEM_WHOLESALE = "EXTRA_CART_ITEM_WHOLESALE" const val EXTRA_CART_ITEM_WHOLESALE = "EXTRA_CART_ITEM_WHOLESALE"
const val EXTRA_CART_ITEM_WHOLESALE_PRICES = "EXTRA_CART_ITEM_WHOLESALE_PRICES"
// Helper methods for starting activity // Helper methods for starting activity
@ -449,13 +472,17 @@ class CheckoutActivity : AppCompatActivity() {
fun startForCart( fun startForCart(
context: Context, context: Context,
cartItemIds: List<Int>, cartItemIds: List<Int>,
isWholesaleArray: BooleanArray? = null isWholesaleArray: BooleanArray? = null,
wholesalePrices: IntArray? = null
) { ) {
val intent = Intent(context, CheckoutActivity::class.java).apply { val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray()) putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
if (isWholesaleArray != null) { if (isWholesaleArray != null) {
putExtra(EXTRA_CART_ITEM_WHOLESALE, isWholesaleArray) putExtra(EXTRA_CART_ITEM_WHOLESALE, isWholesaleArray)
} }
if (wholesalePrices != null) {
putExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES, wholesalePrices)
}
} }
context.startActivity(intent) context.startActivity(intent)
} }

View File

@ -64,7 +64,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
shipPrice = 0, // Will be set when user selects shipping shipPrice = 0, // Will be set when user selects shipping
shipName = "", shipName = "",
shipService = "", shipService = "",
isNego = false, // Default value isNego = false, // Default value
productId = productId, productId = productId,
quantity = quantity, quantity = quantity,
shipEtd = "", shipEtd = "",
@ -93,30 +93,46 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
// Initialize checkout from cart // Initialize checkout from cart
fun initializeFromCart(cartItemIds: List<Int>, isWholesaleMap: Map<Int, Boolean> = emptyMap()) { fun initializeFromCart(
cartItemIds: List<Int>,
isWholesaleMap: Map<Int, Boolean> = emptyMap(),
wholesalePriceMap: Map<Int, Int> = emptyMap()
) {
viewModelScope.launch { viewModelScope.launch {
_isLoading.value = true _isLoading.value = true
try { try {
// Get cart data
val cartResult = repository.getCart() val cartResult = repository.getCart()
if (cartResult is Result.Success) { if (cartResult is Result.Success) {
// Find matching cart items
val matchingItems = mutableListOf<CartItemsItem>() val matchingItems = mutableListOf<CartItemsItem>()
var storeData: DataItemCart? = null var storeData: DataItemCart? = null
for (store in cartResult.data) { for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds } val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
if (storeItems.isNotEmpty()) { if (storeItems.isNotEmpty()) {
matchingItems.addAll(storeItems) // ✅ Apply wholesale prices - Replace item prices with wholesale prices
val updatedItems = storeItems.map { item ->
val wholesalePrice = wholesalePriceMap[item.cartItemId]
val isWholesale = isWholesaleMap[item.cartItemId] ?: false
// Use wholesale price if item is wholesale and price exists
if (isWholesale && wholesalePrice != null) {
Log.d(TAG, "Applying wholesale price for item ${item.cartItemId}: ${item.price} -> $wholesalePrice")
item.copy(price = wholesalePrice)
} else {
Log.d(TAG, "Using regular price for item ${item.cartItemId}: ${item.price}")
item
}
}
matchingItems.addAll(updatedItems)
storeData = store storeData = store
break break
} }
} }
if (matchingItems.isNotEmpty() && storeData != null) { if (matchingItems.isNotEmpty() && storeData != null) {
// Create initial OrderRequest object
val orderRequest = OrderRequest( val orderRequest = OrderRequest(
addressId = 0, addressId = 0,
paymentMethodId = 0, paymentMethodId = 0,
@ -126,21 +142,26 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
isNego = false, isNego = false,
cartItemId = cartItemIds, cartItemId = cartItemIds,
shipEtd = "", shipEtd = "",
// Add a list tracking which items are wholesale isReseller = isWholesaleMap.any { it.value }
isReseller = isWholesaleMap.any { it.value } // Set true if any item is wholesale
) )
// Create checkout data
_checkoutData.value = CheckoutData( _checkoutData.value = CheckoutData(
orderRequest = orderRequest, orderRequest = orderRequest,
productName = matchingItems.first().productName, productName = matchingItems.first().productName,
sellerName = storeData.storeName, sellerName = storeData.storeName,
sellerId = storeData.storeId, sellerId = storeData.storeId,
isBuyNow = false, isBuyNow = false,
cartItems = matchingItems, cartItems = matchingItems, // These now have updated wholesale prices
cartItemWholesaleMap = isWholesaleMap // Store the wholesale map cartItemWholesaleMap = isWholesaleMap
) )
Log.d(TAG, "CheckoutData initialized with ${matchingItems.size} items")
matchingItems.forEachIndexed { index, item ->
val isWholesale = isWholesaleMap[item.cartItemId] ?: false
Log.d(TAG, "Item $index: ${item.productName}, Price: ${item.price}, IsWholesale: $isWholesale")
}
// Calculate totals with updated prices
calculateSubtotal() calculateSubtotal()
calculateTotal() calculateTotal()
} else { } else {
@ -151,6 +172,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} catch (e: Exception) { } catch (e: Exception) {
_errorMessage.value = "Error: ${e.message}" _errorMessage.value = "Error: ${e.message}"
Log.e(TAG, "Error in initializeFromCart", e)
} finally { } finally {
_isLoading.value = false _isLoading.value = false
} }
@ -405,8 +427,6 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
companion object { companion object {
private const val TAG = "CheckoutViewModel" private const val TAG = "CheckoutViewModel"
} }

View File

@ -34,6 +34,8 @@ class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
// Load placeholder image // Load placeholder image
Glide.with(ivProduct.context) Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image) .load(R.drawable.placeholder_image)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct) .into(ivProduct)
} }
} }

View File

@ -22,12 +22,14 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest 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.customer.order.CitiesItem import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
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.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
@ -37,8 +39,8 @@ class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding private lateinit var binding: ActivityAddAddressBinding
private lateinit var apiService: ApiService private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var profileUser: Int = 1
private lateinit var locationManager: LocationManager private lateinit var locationManager: LocationManager
private var profileUserId: Int? = null
private var isRequestingLocation = false private var isRequestingLocation = false
@ -46,6 +48,8 @@ class AddAddressActivity : AppCompatActivity() {
private var longitude: Double? = null private var longitude: Double? = null
private val provinceAdapter by lazy { ProvinceAdapter(this) } private val provinceAdapter by lazy { ProvinceAdapter(this) }
private val cityAdapter by lazy { CityAdapter(this) } private val cityAdapter by lazy { CityAdapter(this) }
private val subdistrictAdapter by lazy { SubdsitrictAdapter(this)}
private val villageAdapter by lazy { VillagesAdapter(this)}
private val viewModel: AddAddressViewModel by viewModels { private val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle -> SavedStateViewModelFactory(this) { savedStateHandle ->
@ -80,11 +84,15 @@ class AddAddressActivity : AppCompatActivity() {
) )
windowInsets windowInsets
} }
viewModel.loadUserProfile()
// Get user profile from session manager viewModel.userProfile.observe(this) { user ->
// profileUser =UserProfile. if (user != null) {
viewModel.userProfile.observe(this){ user -> profileUserId = user.userId
user?.let { updateProfile(it) } Log.d(TAG, "Fetched userId = $profileUserId") // ✅ debug log
} else {
Log.e(TAG, "Error get profile")
}
} }
setupToolbar() setupToolbar()
@ -94,16 +102,10 @@ class AddAddressActivity : AppCompatActivity() {
setupButtonListeners() setupButtonListeners()
setupObservers() setupObservers()
// Force trigger province loading to ensure it happens // Force trigger province loading to ensure it happens
viewModel.getProvinces() viewModel.getProvinces()
} }
private fun updateProfile(userProfile: UserProfile){
profileUser = userProfile.userId
}
// UI setup methods // UI setup methods
private fun setupToolbar() { private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener { binding.toolbar.setNavigationOnClickListener {
@ -116,6 +118,8 @@ class AddAddressActivity : AppCompatActivity() {
// Set adapters // Set adapters
binding.autoCompleteProvinsi.setAdapter(provinceAdapter) binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter) binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villageAdapter)
// Make dropdown appear on click (not just when typing) // Make dropdown appear on click (not just when typing)
binding.autoCompleteProvinsi.setOnClickListener { binding.autoCompleteProvinsi.setOnClickListener {
@ -134,6 +138,26 @@ class AddAddressActivity : AppCompatActivity() {
} }
} }
binding.autoCompleteKecamatan.setOnClickListener{
if (subdistrictAdapter.count > 0){
Log.d(TAG, "Subdistrict clicked, dropdown with ${subdistrictAdapter.count} items")
binding.autoCompleteKecamatan.showDropDown()
} else {
Log.d(TAG, "No kecamatan available")
Toast.makeText(this, "Pilih Kabupaten / Kota terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
binding.autoCompleteDesa.setOnClickListener{
if (villageAdapter.count > 0){
Log.d(TAG, "Village clicked, dropdown with ${villageAdapter.count} items")
binding.autoCompleteDesa.showDropDown()
} else {
Log.d(TAG, "No desa available")
Toast.makeText(this, "Pilih Kecamatan terlebih dahulu", Toast.LENGTH_SHORT).show()
}
}
// Set listeners for selection // Set listeners for selection
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ -> binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position) val provinceId = provinceAdapter.getProvinceId(position)
@ -152,9 +176,41 @@ class AddAddressActivity : AppCompatActivity() {
cityId?.let { id -> cityId?.let { id ->
Log.d(TAG, "Setting selectedCityId=$id") Log.d(TAG, "Setting selectedCityId=$id")
viewModel.getSubdistrict(cityId)
viewModel.selectedCityId = id viewModel.selectedCityId = id
binding.autoCompleteKecamatan.text.clear()
} ?: Log.e(TAG, "Could not get cityId for position $position") } ?: Log.e(TAG, "Could not get cityId for position $position")
} }
binding.autoCompleteKecamatan.setOnItemClickListener { _, _, position, _ ->
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
val subdistrictName = subdistrictAdapter.getSubdistrictName(position)
Log.d(TAG, "Subdistrict selected at position $position, subsId=$subdistrictId")
subdistrictId?.let { id ->
Log.d(TAG, "Setting subdistrict id=$id")
viewModel.getVillages(subdistrictId)
viewModel.selectedSubdistrictId = id
binding.autoCompleteDesa.text.clear()
} ?: Log.e(TAG, "Could not get subsId for position $position")
subdistrictName?.let { name ->
Log.d(TAG, "Setting subdistrict=$name")
viewModel.selectedSubdistrict = name
} ?: Log.e(TAG, "COuldnt get subs name for position ${position}")
}
binding.autoCompleteDesa.setOnItemClickListener { _, _, position, _ ->
val villageId = villageAdapter.getVillageId(position)
Log.d(TAG, "Village selected at position $position, villageId=$villageId")
villageId?.let { id ->
Log.d(TAG, "Setting village=$id")
viewModel.selectedVillages = id
} ?: Log.e(TAG, "Could not get villageId for position $position")
}
} }
private fun setupButtonListeners() { private fun setupButtonListeners() {
@ -178,6 +234,16 @@ class AddAddressActivity : AppCompatActivity() {
handleCityState(state) handleCityState(state)
} }
viewModel.subdistrictState.observe(this) {state ->
Log.d(TAG, "Received subdistrictId update: $state")
handleSubdistrictState(state)
}
viewModel.villagesState.observe(this) {state ->
Log.d(TAG, "Received subdistrictId update: $state")
handleVillageState(state)
}
// Observe address submission // Observe address submission
viewModel.addressSubmissionState.observe(this) { state -> viewModel.addressSubmissionState.observe(this) { state ->
Log.d(TAG, "Received addressSubmissionState update: $state") Log.d(TAG, "Received addressSubmissionState update: $state")
@ -202,7 +268,7 @@ class AddAddressActivity : AppCompatActivity() {
} }
is ViewState.Error -> { is ViewState.Error -> {
// Hide loading indicator // Hide loading indicator
showError("Failed to load provinces: ${state.message}") // showError("Failed to load provinces: ${state.message}")
Log.e("AddAddressActivity", "Province error: ${state.message}") Log.e("AddAddressActivity", "Province error: ${state.message}")
} }
} }
@ -221,12 +287,50 @@ class AddAddressActivity : AppCompatActivity() {
} }
is ViewState.Error -> { is ViewState.Error -> {
binding.cityProgressBar.visibility = View.GONE binding.cityProgressBar.visibility = View.GONE
showError("Failed to load cities: ${state.message}") // showError("Failed to load cities: ${state.message}")
Log.e("AddAddressActivity", "City error: ${state.message}") Log.e("AddAddressActivity", "City error: ${state.message}")
} }
} }
} }
private fun handleSubdistrictState(state: com.alya.ecommerce_serang.data.repository.Result<List<SubdistrictsItem>>) {
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading subdistrict...")
binding.subdistrictProgressBar.visibility = View.VISIBLE
}
is Result.Success -> {
Log.d(TAG, "Subdistrict loaded: ${state.data.size}")
binding.subdistrictProgressBar.visibility = View.GONE
subdistrictAdapter.updateData(state.data)
}
is Result.Error -> {
binding.subdistrictProgressBar.visibility = View.GONE
// showError("Failed to load subs: ${state.message}")
Log.e(TAG, "Subdistrict error: ${state}")
}
}
}
private fun handleVillageState(state: Result<List<VillagesItem>>) {
when (state) {
is Result.Loading -> {
Log.d(TAG, "Loading villages...")
binding.villageProgressBar.visibility = View.VISIBLE
}
is Result.Success -> {
Log.d(TAG, "Villages loaded: ${state.data.size}")
binding.villageProgressBar.visibility = View.GONE
villageAdapter.updateData(state.data)
}
is Result.Error -> {
binding.villageProgressBar.visibility = View.GONE
// showError("Failed to load subs: ${state.message}")
Log.e(TAG, "Village error: ${state}")
}
}
}
private fun handleAddressSubmissionState(state: ViewState<String>) { private fun handleAddressSubmissionState(state: ViewState<String>) {
when (state) { when (state) {
is ViewState.Loading -> { is ViewState.Loading -> {
@ -276,23 +380,20 @@ class AddAddressActivity : AppCompatActivity() {
} }
val street = binding.etDetailAlamat.text.toString().trim() val street = binding.etDetailAlamat.text.toString().trim()
val subDistrict = binding.etKecamatan.text.toString().trim() // val subDistrict = binding.etKecamatan.text.toString().trim()
val postalCode = binding.etKodePos.text.toString().trim() val postalCode = binding.etKodePos.text.toString().trim()
val recipient = binding.etNamaPenerima.text.toString().trim() val recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim() val phone = binding.etNomorHp.text.toString().trim()
val userId = try { val userId = profileUserId
profileUser
} catch (e: Exception) {
Log.w(TAG, "Error getting userId, using default", e)
1 // Default userId for testing
}
val isStoreLocation = false val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId val provinceId = viewModel.selectedProvinceId
val cityId = viewModel.selectedCityId val cityId = viewModel.selectedCityId.toString()
val subDistrict = viewModel.selectedSubdistrict.toString()
val villageId = viewModel.selectedVillages.toString()
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " + Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " + "recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, subdistrict=$subDistrict, villageId=$villageId " +
"lat=$latitude, long=$longitude") "lat=$latitude, long=$longitude")
// Validate required fields // Validate required fields
@ -333,18 +434,19 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields // Create request with all fields
val request = CreateAddressRequest( val request = CreateAddressRequest(
userId = userId!!,
lat = latitude!!, // Safe to use !! as we've checked above lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!, long = longitude!!,
street = street, street = street,
subDistrict = subDistrict, subDistrict = subDistrict,
cityId = cityId, cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId, provId = provinceId,
postCode = postalCode, postCode = postalCode,
idVillage = villageId, // Or provide a real ID if needed
detailAddress = street, detailAddress = street,
userId = userId, isStoreLocation = false,
recipient = recipient, recipient = recipient,
phone = phone, phone = phone
isStoreLocation = isStoreLocation
) )
Log.d(TAG, "Form validation successful, submitting address: $request") Log.d(TAG, "Form validation successful, submitting address: $request")
@ -388,8 +490,8 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi tidak tersedia" binding.tvLocationStatus.text = "Provider lokasi tidak tersedia"
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
showEnableLocationDialog() // showEnableLocationDialog()
return return
} }
@ -416,7 +518,7 @@ class AddAddressActivity : AppCompatActivity() {
latitude = -6.200000 latitude = -6.200000
longitude = 106.816666 longitude = 106.816666
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Timeout lokasi, menggunakan lokasi default", Toast.LENGTH_SHORT).show()
} }
}, 60000) // 15 seconds timeout }, 60000) // 15 seconds timeout
@ -430,7 +532,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${lastLocation.latitude}, ${lastLocation.longitude}" binding.tvLocationStatus.text = "Lokasi terdeteksi: ${lastLocation.latitude}, ${lastLocation.longitude}"
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
return return
} else { } else {
Log.d(TAG, "No last known location, requesting updates") Log.d(TAG, "No last known location, requesting updates")
@ -448,7 +550,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${location.latitude}, ${location.longitude}" binding.tvLocationStatus.text = "Lokasi terdeteksi: ${location.latitude}, ${location.longitude}"
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show() // Toast.makeText(this@AddAddressActivity, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Remove location updates after receiving a location // Remove location updates after receiving a location
try { try {
@ -471,7 +573,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi dimatikan" binding.tvLocationStatus.text = "Provider lokasi dimatikan"
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show() // Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
} }
} }
@ -490,7 +592,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Error: ${e.message}" binding.tvLocationStatus.text = "Error: ${e.message}"
isRequestingLocation = false isRequestingLocation = false
Toast.makeText(this, "Error mendapatkan lokasi: ${e.message}", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Error mendapatkan lokasi: ${e.message}", Toast.LENGTH_SHORT).show()
// Set default location // Set default location
latitude = -6.200000 latitude = -6.200000
@ -518,12 +620,12 @@ class AddAddressActivity : AppCompatActivity() {
// Add button to reload location (add this button to your layout) // Add button to reload location (add this button to your layout)
binding.btnReloadLocation.setOnClickListener { binding.btnReloadLocation.setOnClickListener {
Log.d(TAG, "Reload location button clicked") Log.d(TAG, "Reload location button clicked")
Toast.makeText(this, "Memuat ulang lokasi...", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Memuat ulang lokasi...", Toast.LENGTH_SHORT).show()
requestLocation() requestLocation()
} }
} }
companion object { companion object {
private const val TAG = "AddAddressViewModel" private const val TAG = "AddAddressActivity"
} }
} }

View File

@ -10,6 +10,9 @@ 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.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetail
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
@ -31,15 +34,31 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>() private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
private val _subdistrictState = MutableLiveData<Result<List<SubdistrictsItem>>>()
val subdistrictState: LiveData<Result<List<SubdistrictsItem>>> = _subdistrictState
private val _villagesState = MutableLiveData<Result<List<VillagesItem>>>()
val villagesState: LiveData<Result<List<VillagesItem>>> = _villagesState
private val _userAddress = MutableLiveData<AddressDetail>()
val userAddress: LiveData<AddressDetail> = _userAddress
private val _editAddress = MutableLiveData<Boolean>()
val editAddress: LiveData<Boolean> get() = _editAddress
// Stored in SavedStateHandle for configuration changes // Stored in SavedStateHandle for configuration changes
var selectedProvinceId: Int? var selectedProvinceId: Int?
get() = savedStateHandle.get<Int>("selectedProvinceId") get() = savedStateHandle.get<Int>("selectedProvinceId")
set(value) { savedStateHandle["selectedProvinceId"] = value } set(value) { savedStateHandle["selectedProvinceId"] = value }
var selectedCityId: Int? var selectedCityId: String?
get() = savedStateHandle.get<Int>("selectedCityId") get() = savedStateHandle.get<String>("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value } set(value) { savedStateHandle["selectedCityId"] = value }
var selectedSubdistrict: String? = null
var selectedSubdistrictId: String? = null
var selectedVillages: String? = null
init { init {
// Load provinces on initialization // Load provinces on initialization
getProvinces() getProvinces()
@ -125,20 +144,133 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
} }
} }
fun getSubdistrict(cityId: String) {
_subdistrictState.value = Result.Loading
viewModelScope.launch {
try {
selectedSubdistrictId = cityId
val result = repository.getListSubdistrict(cityId)
result?.let {
_subdistrictState.postValue(Result.Success(it.subdistricts))
Log.d(TAG, "Subdistrict loaded for city $cityId: ${it.subdistricts.size}")
} ?: run {
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "Subdistrict result was null for city $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching subdistrict for city $cityId", e)
}
}
}
fun getVillages(subdistrictId: String) {
_villagesState.value = Result.Loading
viewModelScope.launch {
try {
selectedVillages = subdistrictId
val result = repository.getListVillages(subdistrictId)
result?.let {
_villagesState.postValue(Result.Success(it.villages))
Log.d(TAG, "Villages loaded for subdistrict $subdistrictId: ${it.villages.size}")
} ?: run {
_villagesState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "Village result was null for subdistrict $subdistrictId")
}
} catch (e: Exception) {
_villagesState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching villages for subdistrict $subdistrictId", e)
}
}
}
fun setSelectedProvinceId(id: Int) { fun setSelectedProvinceId(id: Int) {
selectedProvinceId = id selectedProvinceId = id
} }
fun setSelectedCityId(id: Int) { fun detailAddress(addressId: Int){
selectedCityId = id viewModelScope.launch {
try {
val response = repository.getAddressDetail(addressId)
if (response != null){
_userAddress.value = response.address
} else {
Log.e(TAG, "Failed load address detail")
}
} catch (e:Exception){
Log.e(TAG, "Error fetching address detail: $e", e)
}
}
} }
fun loadUserProfile(){ fun updateAddress(oldAddress: AddressDetail, newAddress: AddressDetail) {
val params = buildUpdateBody(oldAddress, newAddress)
if (params.isEmpty()) {
Log.d(TAG, "No changes detected")
_editAddress.value = false
return
}
viewModelScope.launch { viewModelScope.launch {
when (val result = userRepo.fetchUserProfile()){ try {
is Result.Success -> _userProfile.postValue(result.data) val response = repository.updateAddress(oldAddress.id, params)
is Result.Error -> _errorMessageUser.postValue(result.exception.message ?: "Unknown Error") _editAddress.value = response.isSuccessful
is Result.Loading -> null } catch (e: Exception) {
Log.e(TAG, "Error: ${e.message}")
_editAddress.value = false
}
}
}
private fun buildUpdateBody(oldAddress: AddressDetail, newAddress: AddressDetail): Map<String, Any> {
val params = mutableMapOf<String, Any>()
fun addIfChanged(key: String, oldValue: Any?, newValue: Any?) {
if (newValue != null && newValue != oldValue) {
params[key] = newValue
}
}
addIfChanged("street", oldAddress.street, newAddress.street)
addIfChanged("province_id", oldAddress.provinceId, newAddress.provinceId)
addIfChanged("detail", oldAddress.detail, newAddress.detail)
addIfChanged("subdistrict", oldAddress.subdistrict, newAddress.subdistrict)
addIfChanged("city_id", oldAddress.cityId, newAddress.cityId)
addIfChanged("village_id", oldAddress.villageId, newAddress.villageId)
addIfChanged("postal_code", oldAddress.postalCode, newAddress.postalCode)
addIfChanged("phone", oldAddress.phone, newAddress.phone)
addIfChanged("recipient", oldAddress.recipient, newAddress.recipient)
addIfChanged("latitude", oldAddress.latitude, newAddress.latitude)
addIfChanged("longitude", oldAddress.longitude, newAddress.longitude)
addIfChanged("is_store_location", oldAddress.isStoreLocation, newAddress.isStoreLocation)
addIfChanged("village_name", oldAddress.villageName, newAddress.villageName)
addIfChanged("subdsitrict_id", oldAddress.subdistrictId, newAddress.subdistrictId)
addIfChanged("id", oldAddress.id, newAddress.id)
addIfChanged("user_id", oldAddress.userId, newAddress.userId)
addIfChanged("city_name", oldAddress.cityName, newAddress.cityName)
addIfChanged("province_name", oldAddress.provinceName, newAddress.provinceName)
return params
}
fun loadUserProfile() {
viewModelScope.launch {
when (val result = repository.fetchUserProfile()) {
is Result.Success -> {
result.data?.let {
_userProfile.postValue(it) // send UserProfile to LiveData
} ?: _errorMessageUser.postValue("User data not found")
}
is Result.Error -> {
_errorMessageUser.postValue(result.exception.message ?: "Unknown error")
}
is Result.Loading -> {
null
}
} }
} }
} }

View File

@ -74,13 +74,17 @@ class AddressActivity : AppCompatActivity() {
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
adapter = AddressAdapter { address -> adapter = AddressAdapter(
// Select the address in the ViewModel onAddressClick = { address ->
viewModel.selectAddress(address.id) viewModel.selectAddress(address.id)
returnResultAndFinish(address.id)
// Return immediately with the selected address },
returnResultAndFinish(address.id) onEditClick = { address ->
} val intent = Intent(this, EditAddressActivity::class.java)
intent.putExtra(EditAddressActivity.EXTRA_ADDRESS_ID, address.id)
startActivity(intent)
}
)
binding.rvSellerOrder.apply { binding.rvSellerOrder.apply {
layoutManager = LinearLayoutManager(this@AddressActivity) layoutManager = LinearLayoutManager(this@AddressActivity)
@ -119,6 +123,7 @@ class AddressActivity : AppCompatActivity() {
val intent = Intent() val intent = Intent()
intent.putExtra(EXTRA_ADDRESS_ID, addressId) intent.putExtra(EXTRA_ADDRESS_ID, addressId)
setResult(RESULT_OK, intent) setResult(RESULT_OK, intent)
finish()
} }
companion object { companion object {

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.address
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -13,7 +14,8 @@ import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesIte
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
class AddressAdapter( class AddressAdapter(
private val onAddressClick: (AddressesItem) -> Unit private val onAddressClick: (AddressesItem) -> Unit,
private val onEditClick: (AddressesItem) -> Unit
) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) { ) : ListAdapter<AddressesItem, AddressAdapter.AddressViewHolder>(DIFF_CALLBACK) {
private var selectedAddressId: Int? = null private var selectedAddressId: Int? = null
@ -47,18 +49,21 @@ class AddressAdapter(
// Pass the whole address object to provide more context // Pass the whole address object to provide more context
onAddressClick(address) onAddressClick(address)
} }
holder.editButton.setOnClickListener {
onEditClick(address)
}
} }
class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class AddressViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvName: TextView = itemView.findViewById(R.id.tv_name_address) private val tvName: TextView = itemView.findViewById(R.id.tv_name_address)
private val tvDetail: TextView = itemView.findViewById(R.id.tv_detail_address) private val tvDetail: TextView = itemView.findViewById(R.id.tv_detail_address)
val editButton: ImageView = itemView.findViewById(R.id.iv_edit)
private val card: MaterialCardView = itemView as MaterialCardView private val card: MaterialCardView = itemView as MaterialCardView
fun bind(address: AddressesItem, isSelected: Boolean) { fun bind(address: AddressesItem, isSelected: Boolean) {
tvName.text = address.recipient tvName.text = address.recipient
tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}" tvDetail.text = "${address.street}, ${address.subdistrict}, ${address.phone}"
// Make selection more visible
card.strokeWidth = if (isSelected) 3 else 0 card.strokeWidth = if (isSelected) 3 else 0
card.strokeColor = if (isSelected) card.strokeColor = if (isSelected)
ContextCompat.getColor(itemView.context, R.color.blue_400) ContextCompat.getColor(itemView.context, R.color.blue_400)

View File

@ -21,6 +21,8 @@ class AddressViewModel(private val repository: OrderRepository): ViewModel() {
val response = repository.getAddress() val response = repository.getAddress()
response?.let { response?.let {
_addresses.value = it.addresses _addresses.value = it.addresses
?.filter { address -> address.isStoreLocation == false }
?: emptyList()
} }
} }
} }

View File

@ -1,21 +1,458 @@
package com.alya.ecommerce_serang.ui.order.address package com.alya.ecommerce_serang.ui.order.address
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressDetail
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityEditAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class EditAddressActivity : AppCompatActivity() { class EditAddressActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) private lateinit var binding: ActivityEditAddressBinding
enableEdgeToEdge() private lateinit var apiService: ApiService
setContentView(R.layout.activity_edit_address) private lateinit var sessionManager: SessionManager
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) private var latitude: Double? = null
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) private var longitude: Double? = null
insets private var addressId: Int = -1
private var currentAddress: AddressDetail? = null
private val provinceAdapter by lazy { ProvinceAdapter(this) }
private val cityAdapter by lazy { CityAdapter(this) }
private val subdistrictAdapter by lazy { SubdsitrictAdapter(this)}
private val villageAdapter by lazy { VillagesAdapter(this)}
private var provincesList = mutableListOf<ProvincesItem>()
private var citiesList = mutableListOf<CitiesItem>()
private var subdistrictsList = mutableListOf<SubdistrictsItem>()
private var villagesList = mutableListOf<VillagesItem>()
private val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle ->
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = OrderRepository(apiService)
val userRepository = UserRepository(apiService)
AddAddressViewModel(orderRepository, userRepository, savedStateHandle)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
addressId = intent.getIntExtra(EXTRA_ADDRESS_ID, -1)
if (addressId == -1) {
Toast.makeText(this, "Gagal mendapatkan alamat pengguna", Toast.LENGTH_SHORT).show()
finish()
return
}
setupToolbar()
setupAdapters()
setupObservers()
setupListeners()
// Load address detail first
Log.d(TAG, "Loading address with ID: $addressId")
viewModel.detailAddress(addressId)
}
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
private fun setupAdapters() {
// Set custom adapters to AutoCompleteTextViews
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villageAdapter)
}
private fun setupObservers() {
// Observe address detail
viewModel.userAddress.observe(this) { address ->
currentAddress = address
Log.d(TAG, "Address loaded: $address")
populateAddressData(address)
}
// Observe provinces
viewModel.provincesState.observe(this) { viewState ->
when (viewState) {
is ViewState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
is ViewState.Success -> {
binding.progressBar.visibility = View.GONE
provincesList.clear()
provincesList.addAll(viewState.data)
provinceAdapter.updateData(viewState.data)
// Set selected province if address is loaded
currentAddress?.let { address ->
setSelectedProvince(address.provinceId.toInt())
}
}
is ViewState.Error -> {
binding.progressBar.visibility = View.GONE
Log.e(TAG, "Failed to load province ${viewState.message}")
}
}
}
// Observe cities
viewModel.citiesState.observe(this) { viewState ->
when (viewState) {
is ViewState.Loading -> {
binding.cityProgressBar.visibility = View.VISIBLE
}
is ViewState.Success -> {
binding.cityProgressBar.visibility = View.GONE
citiesList.clear()
citiesList.addAll(viewState.data)
cityAdapter.updateData(viewState.data)
// Set selected city if address is loaded
currentAddress?.let { address ->
setSelectedCity(address.cityId)
}
}
is ViewState.Error -> {
binding.cityProgressBar.visibility = View.GONE
Log.e(TAG, "Failed to load cities ${viewState.message}")
}
}
}
// Observe subdistricts
viewModel.subdistrictState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// You can add loading indicator for subdistrict if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
subdistrictsList.clear()
subdistrictsList.addAll(result.data)
subdistrictAdapter.updateData(result.data)
// Set selected subdistrict if address is loaded
currentAddress?.let { address ->
setSelectedSubdistrict(address.subdistrict)
}
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e(TAG, "Failed to load subdistricy")
}
}
}
// Observe villages
viewModel.villagesState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
// You can add loading indicator for village if needed
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
villagesList.clear()
villagesList.addAll(result.data)
villageAdapter.updateData(result.data)
// Set selected village if address is loaded
currentAddress?.let { address ->
setSelectedVillage(address.villageId)
}
}
is Result.Error -> {
Log.e(TAG, "Failed to load villages")
}
}
}
// Observe update result
viewModel.editAddress.observe(this) { isSuccess ->
binding.submitProgressBar.visibility = View.GONE
binding.buttonSimpan.isEnabled = true
if (isSuccess) {
Log.d(TAG, "Address updated successfully")
finish()
} else {
Log.d(TAG, "Failed to update address")
}
}
}
private fun setupListeners() {
// Province selection
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
provinceId?.let { id ->
viewModel.getCities(id)
// Clear dependent dropdowns
clearCitySelection()
clearSubdistrictSelection()
clearVillageSelection()
}
}
// City selection
binding.autoCompleteKabupaten.setOnItemClickListener { _, _, position, _ ->
val cityId = cityAdapter.getCityId(position)
cityId?.let { id ->
viewModel.getSubdistrict(id)
// Clear dependent dropdowns
clearSubdistrictSelection()
clearVillageSelection()
}
}
// Subdistrict selection
binding.autoCompleteKecamatan.setOnItemClickListener { _, _, position, _ ->
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
subdistrictId?.let { id ->
viewModel.getVillages(id)
// Clear dependent dropdowns
clearVillageSelection()
}
}
// Village selection - auto-populate postal code
binding.autoCompleteDesa.setOnItemClickListener { _, _, position, _ ->
// val postalCode = villageAdapter.getPostalCode(position)
// postalCode?.let {
// binding.etKodePos.setText(it)
// }
}
// Save button
binding.buttonSimpan.setOnClickListener {
saveAddress()
}
}
private fun populateAddressData(address: AddressDetail) {
binding.etNamaPenerima.setText(address.recipient ?: "")
binding.etNomorHp.setText(address.phone ?: "")
binding.etDetailAlamat.setText(address.detail ?: "")
binding.etKodePos.setText(address.postalCode ?: "")
// Province will be set when provinces are loaded
// City, subdistrict, village will be set in their respective observers
}
private fun setSelectedProvince(provinceId: Int?) {
provinceId?.let { id ->
val position = provincesList.indexOfFirst { it.provinceId?.toIntOrNull() == id }
if (position >= 0) {
val provinceName = provincesList[position].province
binding.autoCompleteProvinsi.setText(provinceName, false)
viewModel.getCities(id)
}
}
}
private fun setSelectedCity(cityId: String?) {
cityId?.let { id ->
val position = citiesList.indexOfFirst { it.cityId?.toString() == id }
if (position >= 0) {
val cityName = citiesList[position].cityName
binding.autoCompleteKabupaten.setText(cityName, false)
viewModel.getSubdistrict(id)
}
}
}
private fun setSelectedSubdistrict(subdistrictId: String?) {
subdistrictId?.let { id ->
val position = subdistrictsList.indexOfFirst { it.subdistrictId?.toString() == id }
if (position >= 0) {
val subdistrictName = subdistrictsList[position].subdistrictName
binding.autoCompleteKecamatan.setText(subdistrictName, false)
viewModel.getVillages(id)
}
}
}
private fun setSelectedVillage(villageId: String?) {
villageId?.let { id ->
val position = villagesList.indexOfFirst { it.villageId?.toString() == id }
if (position >= 0) {
val villageName = villagesList[position].villageName
binding.autoCompleteDesa.setText(villageName, false)
}
}
}
//
// private fun populatePostalCodeFromVillage(villageId: String?) {
// villageId?.let { id ->
// val village = villagesList.find { it.villageId?.toString() == id }
// village?.postalCode?.let { postalCode ->
// if (binding.etKodePos.text.isNullOrEmpty()) {
// binding.etKodePos.setText(postalCode)
// }
// }
// }
// }
private fun clearCitySelection() {
binding.autoCompleteKabupaten.setText("", false)
citiesList.clear()
cityAdapter.updateData(emptyList())
}
private fun clearSubdistrictSelection() {
binding.autoCompleteKecamatan.setText("", false)
subdistrictsList.clear()
subdistrictAdapter.updateData(emptyList())
}
private fun clearVillageSelection() {
binding.autoCompleteDesa.setText("", false)
binding.etKodePos.setText("") // Clear postal code when village is cleared
villagesList.clear()
villageAdapter.updateData(emptyList())
}
private fun saveAddress() {
currentAddress?.let { oldAddress ->
val newAddress = createNewAddressFromInputs(oldAddress)
binding.submitProgressBar.visibility = View.VISIBLE
binding.buttonSimpan.isEnabled = false
viewModel.updateAddress(oldAddress, newAddress)
} ?: run {
Log.d(TAG, "Address not loaded")
Toast.makeText(this, "Gagal mendapatkan alamat", Toast.LENGTH_SHORT).show()
}
}
private fun createNewAddressFromInputs(oldAddress: AddressDetail): AddressDetail {
val selectedProvinceId = getSelectedProvinceId()
val selectedCityId = getSelectedCityId()
val selectedSubdistrictName = getSelectedSubdistrictName()
val selectedVillageId = getSelectedVillageId()
val selectedSubdistrictId = getSelectedSubdistrictId()
val selectedVillageName = getSelectedVillageName()
val selectedProvinceName = getSelectedProvinceName()
Log.d(TAG, "Old subdistrict: ${oldAddress.subdistrict}")
Log.d(TAG, "Selected subdistrictId: $selectedSubdistrictName")
val newAddress = oldAddress.copy(
recipient = binding.etNamaPenerima.text.toString().trim(),
phone = binding.etNomorHp.text.toString().trim(),
detail = binding.etDetailAlamat.text.toString().trim(),
postalCode = binding.etKodePos.text.toString().trim(),
provinceId = selectedProvinceId?.toString() ?: oldAddress.provinceId,
cityId = selectedCityId ?: oldAddress.cityId,
subdistrict = selectedSubdistrictName ?: oldAddress.subdistrict,
villageId = selectedVillageId,
subdistrictId = selectedSubdistrictId ?: oldAddress.subdistrictId,
villageName = selectedVillageName ?: oldAddress.villageName,
provinceName = selectedProvinceName ?: oldAddress.provinceName
)
// 🔎 Debug logs
Log.d(TAG, "New subdistrict: ${newAddress.subdistrict}")
Log.d(TAG, "New village name: ${newAddress.villageName}")
return newAddress
}
private fun getSelectedProvinceId(): Int? {
val selectedText = binding.autoCompleteProvinsi.text.toString()
val position = provincesList.indexOfFirst { it.province == selectedText }
return if (position >= 0) provinceAdapter.getProvinceId(position) else null
}
private fun getSelectedProvinceName(): String? {
val selectedText = binding.autoCompleteProvinsi.text.toString()
val position = provincesList.indexOfFirst { it.province == selectedText }
return if (position >= 0) provinceAdapter.getProvinceName(position) else null
}
private fun getSelectedCityId(): String? {
val selectedText = binding.autoCompleteKabupaten.text.toString()
val position = citiesList.indexOfFirst { it.cityName == selectedText }
return if (position >= 0) cityAdapter.getCityId(position) else null
}
private fun getSelectedCityName(): String? {
val selectedText = binding.autoCompleteKabupaten.text.toString()
val position = citiesList.indexOfFirst { it.cityName == selectedText }
return if (position >= 0) cityAdapter.getCityName(position) else null
}
private fun getSelectedSubdistrictName(): String? {
val selectedText = binding.autoCompleteKecamatan.text.toString()
val position = subdistrictsList.indexOfFirst { it.subdistrictName == selectedText }
return if (position >= 0) subdistrictAdapter.getSubdistrictName(position) else null
}
private fun getSelectedSubdistrictId(): String? {
val selectedText = binding.autoCompleteKecamatan.text.toString()
val position = subdistrictsList.indexOfFirst { it.subdistrictName == selectedText }
return if (position >= 0) subdistrictAdapter.getSubdistrictId(position) else null
}
private fun getSelectedVillageId(): String? {
val selectedText = binding.autoCompleteDesa.text.toString()
val position = villagesList.indexOfFirst { it.villageName == selectedText }
return if (position >= 0) villageAdapter.getVillageId(position) else null
}
private fun getSelectedVillageName(): String? {
val selectedText = binding.autoCompleteDesa.text.toString()
val position = villagesList.indexOfFirst { it.villageName == selectedText }
return if (position >= 0) villageAdapter.getVillageName(position) else null
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
private const val TAG = "EditAddressActivity"
}
} }

View File

@ -3,8 +3,11 @@ package com.alya.ecommerce_serang.ui.order.address
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Spinner
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
// UI adapters and helpers // UI adapters and helpers
class ProvinceAdapter( class ProvinceAdapter(
@ -12,6 +15,7 @@ class ProvinceAdapter(
resource: Int = android.R.layout.simple_dropdown_item_1line resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) { ) : ArrayAdapter<String>(context, resource, ArrayList()) {
//call from endpoint
private val provinces = ArrayList<ProvincesItem>() private val provinces = ArrayList<ProvincesItem>()
fun updateData(newProvinces: List<ProvincesItem>) { fun updateData(newProvinces: List<ProvincesItem>) {
@ -28,6 +32,10 @@ class ProvinceAdapter(
fun getProvinceId(position: Int): Int? { fun getProvinceId(position: Int): Int? {
return provinces.getOrNull(position)?.provinceId?.toIntOrNull() return provinces.getOrNull(position)?.provinceId?.toIntOrNull()
} }
fun getProvinceName(position: Int): String? {
return provinces.getOrNull(position)?.province?.toString()
}
} }
class CityAdapter( class CityAdapter(
@ -46,7 +54,135 @@ class CityAdapter(
notifyDataSetChanged() notifyDataSetChanged()
} }
fun getCityId(position: Int): Int? { fun getCityId(position: Int): String? {
return cities.getOrNull(position)?.cityId?.toIntOrNull() return cities.getOrNull(position)?.cityId?.toString()
}
fun getCityName(position: Int): String? {
return cities.getOrNull(position)?.cityName?.toString()
}
}
class SubdsitrictAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val cities = ArrayList<SubdistrictsItem>()
fun updateData(newCities: List<SubdistrictsItem>) {
cities.clear()
cities.addAll(newCities)
clear()
addAll(cities.map { it.subdistrictName })
notifyDataSetChanged()
}
fun getSubdistrictId(position: Int): String? {
return cities.getOrNull(position)?.subdistrictId?.toString()
}
fun getSubdistrictName(position: Int): String? {
return cities.getOrNull(position)?.subdistrictName?.toString()
}
}
class VillagesAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val villages = ArrayList<VillagesItem>()
fun updateData(newCities: List<VillagesItem>) {
villages.clear()
villages.addAll(newCities)
clear()
addAll(villages.map { it.villageName })
notifyDataSetChanged()
}
fun getVillageId(position: Int): String? {
return villages.getOrNull(position)?.villageId?.toString()
}
fun getVillageName(position: Int): String? {
return villages.getOrNull(position)?.villageName.toString()
}
fun getPostalCode(position: Int): String?{
return villages.getOrNull(position)?.postalCode
}
}
class BankAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
data class BankItem(
val bankName: String,
val bankCode: String? = null,
val description: String? = null
)
private val banks = ArrayList<BankItem>()
init {
loadHardcodedData()
}
private fun loadHardcodedData() {
val defaultBanks = listOf(
BankItem("Bank Mandiri", "008", "PT Bank Mandiri (Persero) Tbk"),
BankItem("Bank BRI", "002", "PT Bank Rakyat Indonesia (Persero) Tbk"),
BankItem("Bank BCA", "014", "PT Bank Central Asia Tbk"),
BankItem("Bank BNI", "009", "PT Bank Negara Indonesia (Persero) Tbk"),
BankItem("Bank BTN", "200", "PT Bank Tabungan Negara (Persero) Tbk"),
BankItem("Bank CIMB Niaga", "022", "PT Bank CIMB Niaga Tbk"),
BankItem("Bank Danamon", "011", "PT Bank Danamon Indonesia Tbk"),
BankItem("Bank Permata", "013", "PT Bank Permata Tbk"),
BankItem("Bank OCBC NISP", "028", "PT Bank OCBC NISP Tbk"),
BankItem("Bank Maybank", "016", "PT Bank Maybank Indonesia Tbk"),
BankItem("Bank Panin", "019", "PT Bank Panin Dubai Syariah Tbk"),
BankItem("Bank UOB", "023", "PT Bank UOB Indonesia"),
BankItem("Bank Mega", "426", "PT Bank Mega Tbk"),
BankItem("Bank Bukopin", "441", "PT Bank Bukopin Tbk"),
BankItem("Bank BJB", "110", "PT Bank Pembangunan Daerah Jawa Barat dan Banten Tbk")
)
updateData(defaultBanks)
}
fun updateData(newBanks: List<BankItem>) {
banks.clear()
banks.addAll(newBanks)
clear()
addAll(banks.map { it.bankName })
notifyDataSetChanged()
}
fun getBankName(position: Int): String? {
return banks.getOrNull(position)?.bankName
}
fun getBankItem(position: Int): BankItem? {
return banks.getOrNull(position)
}
fun getBankCode(position: Int): String? {
return banks.getOrNull(position)?.bankCode
}
fun findPositionByName(bankName: String): Int {
return banks.indexOfFirst { it.bankName == bankName }
}
fun setDefaultSelection(spinner: Spinner, defaultBankName: String) {
val position = findPositionByName(defaultBankName)
if (position >= 0) {
spinner.setSelection(position)
}
} }
} }

View File

@ -39,7 +39,6 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -63,7 +62,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
private val paymentMethods = arrayOf( private val paymentMethods = arrayOf(
"Transfer Bank", "Transfer Bank",
"E-Wallet",
"QRIS", "QRIS",
) )
@ -129,7 +127,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} }
private fun setupUI() { private fun setupUI() {
val paymentMethods = listOf("Transfer Bank", "COD", "QRIS") val paymentMethods = listOf("Transfer Bank", "QRIS")
val adapter = SpinnerCardAdapter(this, paymentMethods) val adapter = SpinnerCardAdapter(this, paymentMethods)
binding.spinnerPaymentMethod.adapter = adapter binding.spinnerPaymentMethod.adapter = adapter
} }
@ -320,11 +318,12 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return return
} }
binding.etAccountNumber.visibility = View.GONE
if (binding.etAccountNumber.text.toString().trim().isEmpty()) { // if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
return // return
} // }
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") { if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()

View File

@ -10,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrdersItem import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -18,6 +19,13 @@ import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -29,8 +37,8 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private const val TAG = "HistoryViewModel" private const val TAG = "HistoryViewModel"
} }
private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>() // private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>()
val orders: LiveData<ViewState<List<OrdersItem>>> = _orders // val orders: LiveData<ViewState<List<OrdersItem>>> = _orders
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>() private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
@ -59,81 +67,156 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private val _error = MutableLiveData<String>() private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error val error: LiveData<String> get() = _error
fun getOrderList(status: String) { private val _selectedStatus = MutableStateFlow("all")
_orders.value = ViewState.Loading val selectedStatus: StateFlow<String> = _selectedStatus.asStateFlow()
viewModelScope.launch {
try { val orders: StateFlow<ViewState<List<OrdersItem>>> =
if (status == "all") { _selectedStatus
// Get all orders by combining all statuses .flatMapLatest { status ->
getAllOrdersCombined() flow<ViewState<List<OrdersItem>>> {
} else { Log.d(TAG, "⏳ Loading orders for status = $status")
// Get orders for specific status emit(ViewState.Loading)
when (val result = repository.getOrderList(status)) {
is Result.Success -> { val viewState =
_orders.value = ViewState.Success(result.data.orders) if (status == "all") {
Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items") getAllOrdersCombined().also {
Log.d(TAG, "✅ Combined orders size = ${(it as? ViewState.Success)?.data?.size}")
}
} else {
when (val r = repository.getOrderList(status)) {
is Result.Loading -> {
Log.d(TAG, " repository.getOrderList($status) → Loading")
ViewState.Loading
}
is Result.Success -> {
Log.d(TAG, "✅ repository.getOrderList($status) success, size = ${r.data.orders.size}")
// Tag each order so the fragments filter works
val tagged = r.data.orders.onEach { it.displayStatus = status }
ViewState.Success(tagged)
}
is Result.Error -> {
Log.e(TAG, "❌ repository.getOrderList($status) error = ${r.exception.message}")
ViewState.Error(r.exception.message ?: "Unknown error")
}
}
} }
is Result.Error -> {
_orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred") emit(viewState)
Log.e(TAG, "Error loading orders", result.exception)
}
is Result.Loading -> {
// Keep loading state
}
}
} }
} catch (e: Exception) {
_orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
Log.e(TAG, "Exception in getOrderList", e)
} }
} .stateIn(
} viewModelScope,
SharingStarted.WhileSubscribed(5_000),
ViewState.Loading // ② initial value, still fine
)
private suspend fun getAllOrdersCombined() {
try {
val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
val allOrders = mutableListOf<OrdersItem>()
// Use coroutineScope to allow launching async blocks // fun getOrderList(status: String) {
coroutineScope { // _orders.value = ViewState.Loading
val deferreds = allStatuses.map { status -> // viewModelScope.launch {
// try {
// if (status == "all") {
// // Get all orders by combining all statuses
// getAllOrdersCombined()
// } else {
// // Get orders for specific status
// when (val result = repository.getOrderList(status)) {
// is Result.Success -> {
// _orders.value = ViewState.Success(result.data.orders)
// Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items")
// }
// is Result.Error -> {
// _orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
// Log.e(TAG, "Error loading orders", result.exception)
// }
// is Result.Loading -> {
// // Keep loading state
// }
// }
// }
// } catch (e: Exception) {
// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
// Log.e(TAG, "Exception in getOrderList", e)
// }
// }
// }
// private suspend fun getAllOrdersCombined() {
// try {
// val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
// val allOrders = mutableListOf<OrdersItem>()
//
// // Use coroutineScope to allow launching async blocks
// coroutineScope {
// val deferreds = allStatuses.map { status ->
// async {
// when (val result = repository.getOrderList(status)) {
// is Result.Success -> {
// // Tag each order with the status it was fetched from
// result.data.orders.onEach { it.displayStatus = status }
// }
// is Result.Error -> {
// Log.e(TAG, "Error loading orders for status $status", result.exception)
// emptyList<OrdersItem>()
// }
// is Result.Loading -> emptyList<OrdersItem>()
// }
// }
// }
//
// // Await all results and combine
// deferreds.awaitAll().forEach { orders ->
// allOrders.addAll(orders)
// }
// }
//
// // Sort orders
// val sortedOrders = allOrders.sortedByDescending { order ->
// try {
// SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
// } catch (e: Exception) {
// null
// }
// }
//
// _orders.value = ViewState.Success(sortedOrders)
// Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
//
// } catch (e: Exception) {
// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
// Log.e(TAG, "Exception in getAllOrdersCombined", e)
// }
// }
private suspend fun getAllOrdersCombined(): ViewState<List<OrdersItem>> = try {
val statuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
val all = coroutineScope {
statuses
.map { status ->
async { async {
when (val result = repository.getOrderList(status)) { when (val r = repository.getOrderList(status)) {
is Result.Success -> { is Result.Success -> r.data.orders.onEach { it.displayStatus = status }
// Tag each order with the status it was fetched from else -> emptyList()
result.data.orders.onEach { it.displayStatus = status }
}
is Result.Error -> {
Log.e(TAG, "Error loading orders for status $status", result.exception)
emptyList<OrdersItem>()
}
is Result.Loading -> emptyList<OrdersItem>()
} }
} }
} }
.awaitAll()
// Await all results and combine .flatten()
deferreds.awaitAll().forEach { orders ->
allOrders.addAll(orders)
}
}
// Sort orders
val sortedOrders = allOrders.sortedByDescending { order ->
try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
} catch (e: Exception) {
null
}
}
_orders.value = ViewState.Success(sortedOrders)
Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
} catch (e: Exception) {
_orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
Log.e(TAG, "Exception in getAllOrdersCombined", e)
} }
val sorted = all.sortedByDescending { order ->
try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
.parse(order.createdAt)
} catch (_: Exception) { null }
}
ViewState.Success(sorted)
} catch (e: Exception) {
ViewState.Error("Failed to load orders: ${e.message}")
} }
fun confirmOrderCompleted(orderId: Int, status: String) { fun confirmOrderCompleted(orderId: Int, status: String) {
@ -209,9 +292,52 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
fun refreshOrders(status: String = "all") { // fun refreshOrders(status: String = "all") {
Log.d(TAG, "Refreshing orders with status: $status") // Log.d(TAG, "Refreshing orders with status: $status")
// Don't set Loading here if you want to show current data while refreshing // // Don't set Loading here if you want to show current data while refreshing
getOrderList(status) // getOrderList(status)
// }
fun updateStatus(status: String, forceRefresh: Boolean = false) {
Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
// Noop guard (optional): skip if user reselects same tab and no refresh asked
if (_selectedStatus.value == status && !forceRefresh) {
Log.d(TAG, "🔸 Status unchanged & forceRefresh = false → skip update")
return
}
_selectedStatus.value = status
Log.d(TAG, "✅ _selectedStatus set to \"$status\"")
if (forceRefresh) {
Log.d(TAG, "🔄 forceRefresh = true → launching refresh()")
viewModelScope.launch { refresh(status) }
}
} }
private suspend fun refresh(status: String) {
Log.d(TAG, "⏳ refresh(\"$status\") started")
try {
if (status == "all") {
Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
getAllOrdersCombined() // network → cache
} else {
Log.d(TAG, "🌐 repository.getOrderList(\"$status\")")
repository.getOrderList(status) // network → cache
}
Log.d(TAG, "✅ refresh(\"$status\") completed (repository updated)")
// Flow that watches DB/cache will emit automatically
} catch (e: Exception) {
Log.e(TAG, "❌ refresh(\"$status\") failed: ${e.message}", e)
}
}
private fun Result<OrderListResponse>.toViewState(): ViewState<List<OrdersItem>> =
when (this) {
is Result.Success -> ViewState.Success(data.orders)
is Result.Error -> ViewState.Error(exception.message ?: "Unknown error")
is Result.Loading -> ViewState.Loading // should rarely reach UI
}
} }

View File

@ -32,6 +32,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import java.io.File import java.io.File
import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -88,7 +89,8 @@ class OrderHistoryAdapter(
tvStoreName.text = storeName tvStoreName.text = storeName
// Set total amount // Set total amount
tvTotalAmount.text = order.totalAmount tvTotalAmount.text = formatCurrency(order.totalAmount.toDouble())
// Set item count // Set item count
val itemCount = order.orderItems.size val itemCount = order.orderItems.size
@ -195,7 +197,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn) text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener { setOnClickListener {
showCancelOrderDialog(order.orderId.toString()) showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders() // viewModel.refreshOrders()
} }
} }
// deadlineDate.apply { // deadlineDate.apply {
@ -213,14 +215,15 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_processed) text = itemView.context.getString(R.string.dl_processed)
} }
btnLeft.apply { // gabisa complaint
visibility = View.VISIBLE // btnLeft.apply {
text = itemView.context.getString(R.string.canceled_order_btn) // visibility = View.VISIBLE
setOnClickListener { // text = itemView.context.getString(R.string.canceled_order_btn)
showCancelOrderDialog(order.orderId.toString()) // setOnClickListener {
viewModel.refreshOrders() // showCancelOrderDialog(order.orderId.toString())
} // viewModel.refreshOrders()
} // }
// }
} }
"shipped" -> { "shipped" -> {
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang" // Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
@ -237,7 +240,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint) text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener { setOnClickListener {
showCancelOrderDialog(order.orderId.toString()) showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders() // viewModel.refreshOrders()
} }
} }
btnRight.apply { btnRight.apply {
@ -248,7 +251,7 @@ class OrderHistoryAdapter(
// Call ViewModel // Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed") viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders() // viewModel.refreshOrders()
} }
@ -268,13 +271,21 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.dl_shipped) text = itemView.context.getString(R.string.dl_shipped)
} }
btnRight.apply { btnRight.apply {
visibility = View.VISIBLE val checkReview = order.orderItems[0].reviewId
text = itemView.context.getString(R.string.add_review) if (checkReview > 0){
setOnClickListener { visibility = View.VISIBLE
addReviewProduct(order) text = itemView.context.getString(R.string.add_review)
viewModel.refreshOrders() setOnClickListener {
// Handle click event
addReviewProduct(order)
// viewModel.refreshOrders()
// Handle click event
}
} else {
visibility = View.GONE
} }
} }
deadlineDate.apply { deadlineDate.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -518,7 +529,7 @@ class OrderHistoryAdapter(
} }
} }
// Create and show the bottom sheet using the obtained FragmentManager // cancel sebelum bayar
val bottomSheet = CancelOrderBottomSheet( val bottomSheet = CancelOrderBottomSheet(
orderId = orderId, orderId = orderId,
onOrderCancelled = { onOrderCancelled = {
@ -531,6 +542,7 @@ class OrderHistoryAdapter(
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG) bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
} }
// tambah review / ulasan
private fun addReviewProduct(order: OrdersItem) { private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details // Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId) viewModel.getOrderDetails(order.orderId)
@ -550,7 +562,7 @@ class OrderHistoryAdapter(
} }
} }
// Observe the order details result // Observe order items
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems -> viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
if (orderItems != null && orderItems.isNotEmpty()) { if (orderItems != null && orderItems.isNotEmpty()) {
// For single item review // For single item review
@ -589,6 +601,11 @@ class OrderHistoryAdapter(
} }
} }
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
} }
companion object { companion object {

View File

@ -5,8 +5,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -16,6 +21,12 @@ class OrderHistoryFragment : Fragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private val historyVm: HistoryViewModel by activityViewModels {
BaseViewModelFactory {
val api = ApiConfig.getApiService(SessionManager(requireContext()))
HistoryViewModel(OrderRepository(api))
}
}
private lateinit var viewPagerAdapter: OrderViewPagerAdapter private lateinit var viewPagerAdapter: OrderViewPagerAdapter
@ -33,6 +44,8 @@ class OrderHistoryFragment : Fragment() {
sessionManager = SessionManager(requireContext()) sessionManager = SessionManager(requireContext())
setupViewPager() setupViewPager()
} }
private fun setupViewPager() { private fun setupViewPager() {
@ -53,6 +66,16 @@ class OrderHistoryFragment : Fragment() {
else -> "Tab $position" else -> "Tab $position"
} }
}.attach() }.attach()
binding.viewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val status = viewPagerAdapter.orderStatuses[position]
/* setStatus() is the API we added earlier; TRUE → always requery */
historyVm.updateStatus(status, forceRefresh = true)
}
}
)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -8,8 +8,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.data.api.dto.OrdersItem import com.alya.ecommerce_serang.data.api.dto.OrdersItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -27,17 +29,26 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private val viewModel: HistoryViewModel by viewModels { private val viewModel: HistoryViewModel by activityViewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val api = ApiConfig.getApiService(SessionManager(requireContext()))
val orderRepository = OrderRepository(apiService) HistoryViewModel(OrderRepository(api))
HistoryViewModel(orderRepository)
} }
} }
private lateinit var orderAdapter: OrderHistoryAdapter private lateinit var orderAdapter: OrderHistoryAdapter
private var status: String = "all" private var status: String = "all"
private val detailOrderLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
/* forcerefresh the current tab */
viewModel.updateStatus(status, forceRefresh = true)
}
}
companion object { companion object {
private const val ARG_STATUS = "status" private const val ARG_STATUS = "status"
@ -73,8 +84,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
setupRecyclerView() setupRecyclerView()
observeOrderList() observeOrderList()
observeViewModel() observeViewModel()
observeOrderCompletionStatus() // observeOrderCompletionStatus()
loadOrders() // loadOrders()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -96,27 +107,50 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private fun observeOrderList() { private fun observeOrderList() {
// Now we only need to observe one LiveData for all cases // Now we only need to observe one LiveData for all cases
viewModel.orders.observe(viewLifecycleOwner) { result -> // viewModel.orders.observe(viewLifecycleOwner) { result ->
when (result) { // when (result) {
is ViewState.Success -> { // is ViewState.Success -> {
binding.progressBar.visibility = View.GONE // binding.progressBar.visibility = View.GONE
//
if (result.data.isNullOrEmpty()) { // if (result.data.isNullOrEmpty()) {
binding.tvEmptyState.visibility = View.VISIBLE // binding.tvEmptyState.visibility = View.VISIBLE
binding.rvOrders.visibility = View.GONE // binding.rvOrders.visibility = View.GONE
} else { // } else {
binding.tvEmptyState.visibility = View.GONE // binding.tvEmptyState.visibility = View.GONE
binding.rvOrders.visibility = View.VISIBLE // binding.rvOrders.visibility = View.VISIBLE
orderAdapter.submitList(result.data) // orderAdapter.submitList(result.data)
// }
// }
// is ViewState.Error -> {
// binding.progressBar.visibility = View.GONE
// binding.tvEmptyState.visibility = View.VISIBLE
// Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
// }
// is ViewState.Loading -> {
// binding.progressBar.visibility = View.VISIBLE
// }
// }
// }
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.orders.collect { state ->
when (state) {
is ViewState.Loading -> {
binding.progressBar.isVisible = true
}
is ViewState.Error -> {
binding.progressBar.isVisible = false
binding.tvEmptyState.isVisible = true
binding.rvOrders.isVisible = false
Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
}
is ViewState.Success -> {
binding.progressBar.isVisible = false
val list = state.data
.filter { status == "all" || it.displayStatus == status }
binding.tvEmptyState.isVisible = list.isEmpty()
binding.rvOrders.isVisible = list.isNotEmpty()
orderAdapter.submitList(list)
} }
}
is ViewState.Error -> {
binding.progressBar.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
}
is ViewState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
} }
} }
} }
@ -124,51 +158,78 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
private fun observeViewModel() { private fun observeViewModel() {
// Observe order completion // Observe order completion
// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
// when (result) {
// is Result.Success -> {
// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
//// loadOrders() // Refresh here
// }
// is Result.Error -> {
// Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// }
// is Result.Loading -> {
// // Show loading if needed
// }
// }
// }
//
// // Observe cancel order status
// viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
// when (result) {
// is Result.Success -> {
// Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
// loadOrders() // Refresh here
// }
// is Result.Error -> {
// Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// }
// is Result.Loading -> {
// // Show loading if needed
// }
// }
// }
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result -> viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(),
loadOrders() // Refresh here "Order completed!", Toast.LENGTH_SHORT).show()
} viewModel.updateStatus(status, forceRefresh = true)
is Result.Error -> {
Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading if needed
} }
is Result.Error ->
Toast.makeText(requireContext(),
"Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
else -> { /* Loading → no UI change */ }
} }
} }
// Observe cancel order status
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result -> viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(),
loadOrders() // Refresh here "Order cancelled!", Toast.LENGTH_SHORT).show()
} viewModel.updateStatus(status, forceRefresh = true)
is Result.Error -> {
Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading if needed
} }
is Result.Error ->
Toast.makeText(requireContext(),
"Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
else -> { /* Loading */ }
} }
} }
} }
private fun loadOrders() { // private fun loadOrders() {
// Simple - just call getOrderList for any status including "all" // // Simple - just call getOrderList for any status including "all"
viewModel.getOrderList(status) // viewModel.getOrderList(status)
} // }
private val detailOrderLauncher = registerForActivityResult( // private val detailOrderLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() // ActivityResultContracts.StartActivityForResult()
) { result -> // ) { result ->
if (result.resultCode == Activity.RESULT_OK) { // if (result.resultCode == Activity.RESULT_OK) {
// Refresh order list when returning with OK result // // Refresh order list when returning with OK result
loadOrders() //// loadOrders()
} // }
} // }
private fun navigateToOrderDetail(order: OrdersItem) { private fun navigateToOrderDetail(order: OrdersItem) {
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply { val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
@ -183,7 +244,9 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) { override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
if (success) { if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
loadOrders() // Refresh the list // loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true)
} else { } else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
} }
@ -192,7 +255,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) { override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
if (success) { if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
loadOrders() // Refresh the list // loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true)
} else { } else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
} }
@ -207,20 +271,20 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
_binding = null _binding = null
} }
private fun observeOrderCompletionStatus() { // private fun observeOrderCompletionStatus() {
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result -> // viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
when (result) { // when (result) {
is Result.Loading -> { // is Result.Loading -> {
// Handle loading state if needed // // Handle loading state if needed
} // }
is Result.Success -> { // is Result.Success -> {
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show() // Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
loadOrders() //// loadOrders()
} // }
is Result.Error -> { // is Result.Error -> {
Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show() // Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
} // }
} // }
} // }
} // }
} }

View File

@ -10,6 +10,8 @@ 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.response.customer.order.OrderItemsItem import com.alya.ecommerce_serang.data.api.response.customer.order.OrderItemsItem
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import java.text.NumberFormat
import java.util.Locale
class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductViewHolder>() { class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductViewHolder>() {
@ -46,7 +48,7 @@ class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductView
tvQuantity.text = "${product.quantity} buah" tvQuantity.text = "${product.quantity} buah"
// Set price with currency format // Set price with currency format
tvProductPrice.text = formatCurrency(product.price) tvProductPrice.text = formatCurrency(product.price.toDouble())
val fullImageUrl = when (val img = product.productImage) { val fullImageUrl = when (val img = product.productImage) {
is String -> { is String -> {
@ -65,10 +67,9 @@ class OrderProductAdapter : RecyclerView.Adapter<OrderProductAdapter.ProductView
private fun formatCurrency(amount: Int): String { private fun formatCurrency(amount: Double): String {
// In a real app, you would use NumberFormat for proper currency formatting val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
// For simplicity, just return a basic formatted string return formatter.format(amount).replace(",00", "")
return "Rp${amount}"
} }
} }
} }

View File

@ -9,7 +9,7 @@ class OrderViewPagerAdapter(
) : FragmentStateAdapter(fragmentActivity) { ) : FragmentStateAdapter(fragmentActivity) {
// Define all possible order statuses // Define all possible order statuses
private val orderStatuses = listOf( val orderStatuses = listOf(
"all", // All orders "all", // All orders
"unpaid", // Menunggu Tagihan "unpaid", // Menunggu Tagihan
"paid", // Belum Dibayar "paid", // Belum Dibayar

View File

@ -55,7 +55,7 @@ class CancelOrderBottomSheet(
val btnConfirm = view.findViewById<Button>(R.id.btn_confirm) val btnConfirm = view.findViewById<Button>(R.id.btn_confirm)
// Set the title // Set the title
tvTitle.text = "Cancel Order #$orderId" tvTitle.text = "Batalkan Pesanan #$orderId"
// Set up the spinner with cancellation reasons // Set up the spinner with cancellation reasons
setupReasonSpinner(spinnerReason) setupReasonSpinner(spinnerReason)
@ -94,11 +94,11 @@ class CancelOrderBottomSheet(
private fun getCancellationReasons(): List<CancelOrderReq> { private fun getCancellationReasons(): List<CancelOrderReq> {
// These should ideally come from the server or a configuration // These should ideally come from the server or a configuration
return listOf( return listOf(
CancelOrderReq(1, "Changed my mind"), CancelOrderReq(1, "Berubah pikiran"),
CancelOrderReq(2, "Found a better option"), CancelOrderReq(2, "Menemukan pilihan yang lebih baik"),
CancelOrderReq(3, "Ordered by mistake"), CancelOrderReq(3, "Kesalahan pemesanan"),
CancelOrderReq(4, "Delivery time too long"), CancelOrderReq(4, "Waktu pengiriman lama"),
CancelOrderReq(5, "Other reason") CancelOrderReq(5, "Lainnya")
) )
} }

View File

@ -44,6 +44,9 @@ import com.google.gson.Gson
import java.io.File import java.io.File
import java.text.NumberFormat import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@ -197,12 +200,12 @@ class DetailOrderStatusActivity : AppCompatActivity() {
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}") Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
// Set subtotal, shipping cost, and total // Set subtotal, shipping cost, and total
val subtotal = orders.totalAmount?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0 // val subtotal = orders.totalAmount?.minus(orders.shipmentPrice.toDouble() ?: 0) ?: 0
binding.tvSubtotal.text = formatCurrency(subtotal.toDouble()) // binding.tvSubtotal.text = formatCurrency(subtotal.toDouble())
binding.tvShippingCost.text = formatCurrency(orders.shipmentPrice.toDouble()) binding.tvShippingCost.text = formatCurrency(orders.shipmentPrice.toDouble())
binding.tvTotal.text = formatCurrency(orders.totalAmount?.toDouble() ?: 0.00) binding.tvTotal.text = formatCurrency(orders.totalAmount?.toDouble() ?: 0.00)
Log.d(TAG, "populateOrderDetails: Subtotal=$subtotal, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}") Log.d(TAG, "populateOrderDetails: Subtotal=, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}")
// Adjust buttons based on order status // Adjust buttons based on order status
Log.d(TAG, "populateOrderDetails: Adjusting buttons for status=$orderStatus") Log.d(TAG, "populateOrderDetails: Adjusting buttons for status=$orderStatus")
@ -223,6 +226,11 @@ class DetailOrderStatusActivity : AppCompatActivity() {
this.adapter = adapter this.adapter = adapter
} }
adapter.submitList(orderItems) adapter.submitList(orderItems)
// get data from ordetlistitemsitem untuk ambil subtotal nya dan dijumlahkan
val subtotalSum = orderItems.sumOf { it.subtotal }
binding.tvSubtotal.text = formatCurrency(subtotalSum.toDouble())
} }
private fun adjustButtonsBasedOnStatus(orders: Orders, status: String) { private fun adjustButtonsBasedOnStatus(orders: Orders, status: String) {
@ -287,20 +295,20 @@ class DetailOrderStatusActivity : AppCompatActivity() {
// Show status note // Show status note
binding.tvStatusHeader.text = "Sudah Dibayar" binding.tvStatusHeader.text = "Sudah Dibayar"
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePay(orders.updatedAt)}" binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePaid(orders.updatedAt)}"
binding.tvPaymentDeadlineLabel.text = "Batas konfirmasi penjual:" binding.tvPaymentDeadlineLabel.text = "Batas konfirmasi penjual:"
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt) binding.tvPaymentDeadline.text = formatDatePaid(orders.updatedAt)
// Set buttons // cancel pesanan
binding.btnSecondary.apply { // binding.btnSecondary.apply {
visibility = View.VISIBLE // visibility = View.VISIBLE
text = "Batalkan Pesanan" // text = "Batalkan Pesanan"
setOnClickListener { // setOnClickListener {
Log.d(TAG, "Cancel Order button clicked") // Log.d(TAG, "Cancel Order button clicked")
showCancelOrderDialog(orders.orderId.toString()) // showCancelOrderDialog(orders.orderId.toString())
viewModel.getOrderDetails(orders.orderId) // viewModel.getOrderDetails(orders.orderId)
} // }
} // }
} }
"processed" -> { "processed" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
@ -309,7 +317,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda" binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
binding.tvPaymentDeadlineLabel.text = "Batas diproses penjual:" binding.tvPaymentDeadlineLabel.text = "Batas diproses penjual:"
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt) binding.tvPaymentDeadline.text = formatDateProcessed(orders.updatedAt)
binding.btnSecondary.apply { binding.btnSecondary.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -333,7 +341,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}" binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
binding.tvPaymentDeadlineLabel.text = "Estimasi pesanan sampai:" binding.tvPaymentDeadlineLabel.text = "Estimasi pesanan sampai:"
binding.tvPaymentDeadline.text = formatShipmentDate(orders.updatedAt, orders.etd ?: "0") binding.tvPaymentDeadline.text = formatShipmentDate(orders.autoCompletedAt, orders.etd ?: "0")
binding.btnSecondary.apply { binding.btnSecondary.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -367,7 +375,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
binding.tvStatusHeader.text = "Pesanan Selesai" binding.tvStatusHeader.text = "Pesanan Selesai"
binding.tvStatusNote.visibility = View.GONE binding.tvStatusNote.visibility = View.GONE
binding.tvPaymentDeadlineLabel.text = "Pesanan selesai:" binding.tvPaymentDeadlineLabel.text = "Pesanan selesai:"
binding.tvPaymentDeadline.text = formatDate(orders.autoCompletedAt.toString()) binding.tvPaymentDeadline.text = formatDate(orders.updatedAt.toString())
binding.btnPrimary.apply { binding.btnPrimary.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
@ -386,7 +394,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
"canceled" -> { "canceled" -> {
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order") Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
binding.tvStatusHeader.text = "Pesanan Selesai" binding.tvStatusHeader.text = "Pesanan Dibatalkan"
binding.tvStatusNote.visibility = View.VISIBLE binding.tvStatusNote.visibility = View.VISIBLE
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}" binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
binding.tvPaymentDeadlineLabel.text = "Tanggal dibatalkan: " binding.tvPaymentDeadlineLabel.text = "Tanggal dibatalkan: "
@ -598,10 +606,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
val bottomSheet = CancelOrderBottomSheet( val bottomSheet = CancelOrderBottomSheet(
orderId = orderId, orderId = orderId,
onOrderCancelled = { onOrderCancelled = {
// Handle the successful cancellation
// Refresh the data
// Show a success message
Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
} }
) )
@ -610,32 +614,17 @@ class DetailOrderStatusActivity : AppCompatActivity() {
} }
private fun formatDate(dateString: String): String { private fun formatDate(dateString: String): String {
Log.d(TAG, "formatDate: Formatting date: $dateString")
return try { return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) val jakarta = ZoneId.of("Asia/Jakarta")
inputFormat.timeZone = TimeZone.getTimeZone("UTC") val instant = Instant.parse(dateString) // parses ISO8601 with Z
val zoned = instant.atZone(jakarta)
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID")) val time = DateTimeFormatter.ofPattern("HH:mm", Locale("id", "ID")).format(zoned)
val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID")) val date = DateTimeFormatter.ofPattern("dd MMMM yyyy",Locale("id", "ID")).format(zoned)
val date = inputFormat.parse(dateString) "$time\n$date"
date?.let {
val calendar = Calendar.getInstance()
calendar.time = it
calendar.set(Calendar.HOUR_OF_DAY, 23)
calendar.set(Calendar.MINUTE, 59)
val timePart = timeFormat.format(calendar.time)
val datePart = dateFormat.format(calendar.time)
val formatted = "$timePart\n$datePart"
Log.d(TAG, "formatDate: Formatted date: $formatted")
formatted
} ?: dateString
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "formatDate: Error formatting date: ${e.message}", e) Log.e(TAG, "formatDate: $e")
dateString dateString
} }
} }
@ -673,6 +662,73 @@ class DetailOrderStatusActivity : AppCompatActivity() {
} }
} }
private fun formatDatePaid(dateString: String): String {
Log.d(TAG, "formatDatePay: Formatting payment date: $dateString")
return try {
// Parse the ISO 8601 date
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
val createdDate = isoDateFormat.parse(dateString)
// Add 24 hours to get due date
val calendar = Calendar.getInstance()
calendar.time = createdDate
calendar.add(Calendar.HOUR, 120)
val dueDate = calendar.time
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
val timePart = timeFormat.format(dueDate)
val datePart = dateFormat.format(dueDate)
val formatted = "$timePart\n$datePart"
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
formatted
} catch (e: Exception) {
Log.e(TAG, "formatDatePay: Error formatting date: ${e.message}", e)
dateString
}
}
//format batas tgl diproses
private fun formatDateProcessed(dateString: String): String {
Log.d(TAG, "formatDatePay: Formatting payment date: $dateString")
return try {
// Parse the ISO 8601 date
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
val createdDate = isoDateFormat.parse(dateString)
// Add 24 hours to get due date
val calendar = Calendar.getInstance()
calendar.time = createdDate
calendar.add(Calendar.HOUR, 72)
val dueDate = calendar.time
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
val timePart = timeFormat.format(dueDate)
val datePart = dateFormat.format(dueDate)
val formatted = "$timePart\n$datePart"
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
formatted
} catch (e: Exception) {
Log.e(TAG, "formatDatePay: Error formatting date: ${e.message}", e)
dateString
}
}
private fun formatShipmentDate(dateString: String, estimateString: String): String { private fun formatShipmentDate(dateString: String, estimateString: String): String {
Log.d(TAG, "formatShipmentDate: Formatting shipment date: $dateString with ETD: $estimateString") Log.d(TAG, "formatShipmentDate: Formatting shipment date: $dateString with ETD: $estimateString")
@ -696,7 +752,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
calendar.time = it calendar.time = it
// Add estimated days // Add estimated days
calendar.add(Calendar.DAY_OF_MONTH, estimate)
val formatted = outputFormat.format(calendar.time) val formatted = outputFormat.format(calendar.time)
Log.d(TAG, "formatShipmentDate: Estimated arrival date: $formatted") Log.d(TAG, "formatShipmentDate: Estimated arrival date: $formatted")

View File

@ -75,6 +75,7 @@ class DetailOrderViewModel(private val orderRepository: OrderRepository): ViewMo
orderRepository.submitComplaint(orderId.toString(), reason, imageFile) orderRepository.submitComplaint(orderId.toString(), reason, imageFile)
_isSuccess.value = true _isSuccess.value = true
_message.value = "Order canceled successfully" _message.value = "Order canceled successfully"
Log.d("DetailOrderViewModel", "Complaint order success")
} catch (e: Exception) { } catch (e: Exception) {
_isSuccess.value = false _isSuccess.value = false

View File

@ -164,6 +164,7 @@ class DetailProductActivity : AppCompatActivity() {
} }
} }
//info toko
private fun updateStoreInfo(store: StoreItem?) { private fun updateStoreInfo(store: StoreItem?) {
store?.let { store?.let {
binding.tvSellerName.text = it.storeName binding.tvSellerName.text = it.storeName
@ -230,9 +231,8 @@ class DetailProductActivity : AppCompatActivity() {
private fun updateUI(product: Product){ private fun updateUI(product: Product){
binding.tvProductName.text = product.productName binding.tvProductName.text = product.productName
binding.tvPrice.text = "Rp${formatCurrency(product.price.toDouble())}" binding.tvPrice.text = formatCurrency(product.price.toDouble())
binding.tvSold.text = "Terjual ${product.totalSold} buah" binding.tvSold.text = "Terjual ${product.totalSold} buah"
binding.tvRating.text = product.rating
binding.tvWeight.text = "${product.weight} gram" binding.tvWeight.text = "${product.weight} gram"
binding.tvStock.text = "${product.stock} buah" binding.tvStock.text = "${product.stock} buah"
binding.tvCategory.text = product.productCategory binding.tvCategory.text = product.productCategory
@ -243,7 +243,7 @@ class DetailProductActivity : AppCompatActivity() {
isWholesaleSelected = false // Default to regular pricing isWholesaleSelected = false // Default to regular pricing
if (isWholesaleAvailable) { if (isWholesaleAvailable) {
binding.containerWholesale.visibility = View.VISIBLE binding.containerWholesale.visibility = View.VISIBLE
binding.tvPriceWholesale.text = "Rp${formatCurrency(product.wholesalePrice!!.toDouble())}" binding.tvPriceWholesale.text = formatCurrency(product.wholesalePrice!!.toDouble())
binding.descMinOrder.text = "Minimal pembelian ${minOrder}" binding.descMinOrder.text = "Minimal pembelian ${minOrder}"
} else { } else {
binding.containerWholesale.visibility = View.GONE binding.containerWholesale.visibility = View.GONE
@ -281,6 +281,17 @@ class DetailProductActivity : AppCompatActivity() {
.load(fullImageUrl) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage) .into(binding.ivProductImage)
val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull()
if (ratingValue != null && ratingValue > 0f) {
binding.tvRating.text = String.format("%.1f", ratingValue)
binding.tvRating.visibility = View.VISIBLE
} else {
binding.tvRating.text = "Belum ada rating"
binding.tvRating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
}
} }
private fun handleAllReviewsClick(productId: Int) { private fun handleAllReviewsClick(productId: Int) {
@ -347,6 +358,7 @@ class DetailProductActivity : AppCompatActivity() {
} }
//dialog tambah quantity dan harga grosir
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) { private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
val bottomSheetDialog = BottomSheetDialog(this) val bottomSheetDialog = BottomSheetDialog(this)
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null) val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
@ -377,10 +389,9 @@ class DetailProductActivity : AppCompatActivity() {
switchWholesale.visibility = View.VISIBLE switchWholesale.visibility = View.VISIBLE
Toast.makeText(this, "Minimal pembelian grosir $currentQuantity produk", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Minimal pembelian grosir $currentQuantity produk", Toast.LENGTH_SHORT).show()
} else { } else {
titleWholesale.visibility = View.GONE
switchWholesale.visibility = View.GONE switchWholesale.visibility = View.GONE
} }
// Set initial quantity based on current selection
switchWholesale.setOnCheckedChangeListener { _, isChecked -> switchWholesale.setOnCheckedChangeListener { _, isChecked ->
isWholesaleSelected = isChecked isWholesaleSelected = isChecked

View File

@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.product
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -35,7 +36,16 @@ class OtherProductAdapter (
tvProductName.text = product.name tvProductName.text = product.name
tvProductPrice.text = formatCurrency(product.price.toDouble()) tvProductPrice.text = formatCurrency(product.price.toDouble())
rating.text = product.rating val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull()
if (ratingValue != null && ratingValue > 0f) {
binding.rating.text = String.format("%.1f", ratingValue)
binding.rating.visibility = View.VISIBLE
} else {
binding.rating.text = "Belum ada rating"
binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
}
// Load image using Glide // Load image using Glide
Glide.with(itemView) Glide.with(itemView)

View File

@ -128,7 +128,6 @@ class StoreDetailActivity : AppCompatActivity() {
private fun updateStoreInfo(store: StoreItem?) { private fun updateStoreInfo(store: StoreItem?) {
store?.let { store?.let {
binding.tvStoreName.text = it.storeName binding.tvStoreName.text = it.storeName
binding.tvStoreRating.text = it.storeRating
binding.tvStoreLocation.text = it.storeLocation binding.tvStoreLocation.text = it.storeLocation
binding.tvStoreType.text = it.storeType binding.tvStoreType.text = it.storeType
binding.tvActiveStatus.text = it.status binding.tvActiveStatus.text = it.status
@ -145,6 +144,17 @@ class StoreDetailActivity : AppCompatActivity() {
.load(fullImageUrl) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.ivStoreImage) .into(binding.ivStoreImage)
val ratingStr = it.storeRating
val ratingValue = ratingStr?.toFloatOrNull()
if (ratingValue != null && ratingValue > 0f) {
binding.tvStoreRating.text = String.format("%.1f", ratingValue)
binding.tvStoreRating.visibility = View.VISIBLE
} else {
binding.tvStoreRating.text = "Belum ada rating"
binding.tvStoreRating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
}
} }
} }

View File

@ -16,15 +16,19 @@ import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.auth.LoginActivity import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.auth.RegisterStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.order.address.AddressActivity import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreOnReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreSuspendedActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -44,6 +48,14 @@ class ProfileFragment : Fragment() {
} }
} }
private val myStoreViewModel: MyStoreViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val myStoreRepository = MyStoreRepository(apiService)
MyStoreViewModel(myStoreRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext()) sessionManager = SessionManager(requireContext())
@ -59,26 +71,33 @@ class ProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeUserProfile() observeUserProfile()
observeStoreStatus()
viewModel.loadUserProfile() viewModel.loadUserProfile()
viewModel.checkStoreUser() viewModel.checkStoreUser()
binding.cardBukaToko.setOnClickListener{ binding.cardBukaToko.setOnClickListener{
val hasStore = viewModel.checkStore.value // if (hasStore == true) startActivity(Intent(requireContext(), MyStoreActivity::class.java))
// else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
Log.d("Profile Fragment", "Check store $hasStore") if (viewModel.checkStore.value == true) {
myStoreViewModel.loadMyStore()
if (hasStore == true){ myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { store ->
binding.tvBukaToko.text = "Lihat Toko Saya" store?.let {
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java) when (store.storeStatus) {
startActivity(intentBuka) "process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java))
} else { "active" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
binding.tvBukaToko.text = "Buka Toko" "inactive" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java) "suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java))
startActivity(intentBuka) else -> startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
} }
} ?: run {
Toast.makeText(requireContext(), "Gagal memuat data toko", Toast.LENGTH_SHORT).show()
}
}
} else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
} }
binding.btnDetailProfile.setOnClickListener{ binding.btnDetailProfile.setOnClickListener{
@ -96,15 +115,14 @@ class ProfileFragment : Fragment() {
startActivity(intent) startActivity(intent)
} }
binding.cardLogout.setOnClickListener({ binding.cardLogout.setOnClickListener{
logout() logout()
}
}) binding.cardAddress.setOnClickListener{
binding.cardAddress.setOnClickListener({
val intent = Intent(requireContext(), AddressActivity::class.java) val intent = Intent(requireContext(), AddressActivity::class.java)
startActivity(intent) startActivity(intent)
}) }
} }
private fun observeUserProfile() { private fun observeUserProfile() {
@ -116,6 +134,12 @@ class ProfileFragment : Fragment() {
} }
} }
private fun observeStoreStatus() {
viewModel.checkStore.observe(viewLifecycleOwner) { hasStore ->
binding.tvBukaToko.text = if (hasStore) "Toko Saya" else "Buka Toko"
}
}
private fun updateUI(user: UserProfile) = with(binding){ private fun updateUI(user: UserProfile) = with(binding){
val fullImageUrl = when (val img = user.image) { val fullImageUrl = when (val img = user.image) {
is String -> { is String -> {

View File

@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.ui.profile.editprofile package com.alya.ecommerce_serang.ui.profile.editprofile
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
@ -159,17 +161,14 @@ class EditProfileCustActivity : AppCompatActivity() {
} }
private fun openImagePicker() { private fun openImagePicker() {
// Check for permission first val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission( Manifest.permission.READ_MEDIA_IMAGES
this, } else {
android.Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED }
) {
ActivityCompat.requestPermissions( if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
this, ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_STORAGE_PERMISSION)
arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_STORAGE_PERMISSION
)
} else { } else {
launchImagePicker() launchImagePicker()
} }

View File

@ -20,7 +20,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
import com.alya.ecommerce_serang.ui.profile.mystore.chat.ChatListStoreActivity import com.alya.ecommerce_serang.ui.profile.mystore.chat.ChatListStoreActivity
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity import com.alya.ecommerce_serang.ui.profile.mystore.profile.DetailStoreProfileActivity
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
@ -51,14 +51,16 @@ class MyStoreActivity : AppCompatActivity() {
enableEdgeToEdge() enableEdgeToEdge()
binding.header.headerTitle.text = "Toko Saya"
binding.header.headerLeftIcon.setOnClickListener { binding.headerMyStore.headerTitle.text = "Toko Saya"
binding.headerMyStore.headerLeftIcon.setOnClickListener {
onBackPressed() onBackPressed()
finish() finish()
} }
viewModel.loadMyStore() viewModel.loadMyStore()
viewModel.loadMyStoreProducts()
viewModel.myStoreProfile.observe(this){ user -> viewModel.myStoreProfile.observe(this){ user ->
user?.let { myStoreProfileOverview(it) } user?.let { myStoreProfileOverview(it) }
@ -67,9 +69,9 @@ class MyStoreActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { error -> viewModel.errorMessage.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show() Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
} }
setUpClickListeners() setUpClickListeners()
getCountOrder() getCountOrder()
observeViewModel()
viewModel.fetchBalance() viewModel.fetchBalance()
fetchBalance() fetchBalance()
} }
@ -105,25 +107,21 @@ class MyStoreActivity : AppCompatActivity() {
} }
binding.tvHistory.setOnClickListener { binding.tvHistory.setOnClickListener {
val intent = Intent(this, SellsActivity::class.java) startActivity(Intent(this, SellsActivity::class.java))
startActivity(intent)
} }
binding.layoutPerluTagihan.setOnClickListener { binding.layoutPerluTagihan.setOnClickListener {
val intent = Intent(this, SellsActivity::class.java) startActivity(Intent(this, SellsActivity::class.java))
startActivity(intent)
//navigateToSellsFragment("pending") //navigateToSellsFragment("pending")
} }
binding.layoutPembayaran.setOnClickListener { binding.layoutPembayaran.setOnClickListener {
val intent = Intent(this, SellsActivity::class.java) startActivity(Intent(this, SellsActivity::class.java))
startActivity(intent)
//navigateToSellsFragment("paid") //navigateToSellsFragment("paid")
} }
binding.layoutPerluDikirim.setOnClickListener { binding.layoutPerluDikirim.setOnClickListener {
val intent = Intent(this, SellsActivity::class.java) startActivity(Intent(this, SellsActivity::class.java))
startActivity(intent)
//navigateToSellsFragment("processed") //navigateToSellsFragment("processed")
} }
@ -132,15 +130,11 @@ class MyStoreActivity : AppCompatActivity() {
} }
binding.layoutReview.setOnClickListener { binding.layoutReview.setOnClickListener {
supportFragmentManager.beginTransaction() startActivity(Intent(this, ReviewActivity::class.java))
.replace(android.R.id.content, ReviewFragment())
.addToBackStack(null)
.commit()
} }
binding.layoutInbox.setOnClickListener { binding.layoutInbox.setOnClickListener {
val intent = Intent(this, ChatListStoreActivity::class.java) startActivity(Intent(this, ChatListStoreActivity::class.java))
startActivity(intent)
} }
} }
@ -177,13 +171,11 @@ class MyStoreActivity : AppCompatActivity() {
when (result) { when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> is com.alya.ecommerce_serang.data.repository.Result.Loading ->
null null
// binding.progressBar.isVisible = true
is com.alya.ecommerce_serang.data.repository.Result.Success -> is com.alya.ecommerce_serang.data.repository.Result.Success ->
viewModel.formattedBalance.observe(this) { viewModel.formattedBalance.observe(this) {
binding.tvBalance.text = it binding.tvBalance.text = it
} }
is Result.Error -> { is Result.Error -> {
// binding.progressBar.isVisible = false
Log.e( Log.e(
"MyStoreActivity", "MyStoreActivity",
"Gagal memuat saldo: ${result.exception.localizedMessage}" "Gagal memuat saldo: ${result.exception.localizedMessage}"
@ -193,15 +185,29 @@ class MyStoreActivity : AppCompatActivity() {
} }
} }
private fun observeViewModel() {
viewModel.productList.observe(this) { result ->
when (result) {
is Result.Loading -> {
null
}
is Result.Success -> {
val productList = result.data
val count = productList.size
Log.d("MyStoreActivty", "You have $count products")
// Example: update UI
binding.tvNumProduct.text = "$count produk"
}
is Result.Error -> {
Log.e("MyStoreActivity", "Failed load product : ${result.exception.message}" )
}
}
}
}
companion object { companion object {
private const val PROFILE_REQUEST_CODE = 100 private const val PROFILE_REQUEST_CODE = 100
} }
// private fun navigateToSellsFragment(status: String) {
// val sellsFragment = SellsListFragment.newInstance(status)
// supportFragmentManager.beginTransaction()
// .replace(android.R.id.content, sellsFragment)
// .addToBackStack(null)
// .commit()
// }
} }

View File

@ -1,7 +1,5 @@
package com.alya.ecommerce_serang.ui.auth package com.alya.ecommerce_serang.ui.profile.mystore
import android.Manifest
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@ -21,20 +19,24 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding
import com.alya.ecommerce_serang.ui.order.address.BankAdapter
import com.alya.ecommerce_serang.ui.order.address.CityAdapter import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
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.RegisterStoreViewModel
class RegisterStoreActivity : AppCompatActivity() { class RegisterStoreActivity : AppCompatActivity() {
@ -43,20 +45,21 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var provinceAdapter: ProvinceAdapter private lateinit var provinceAdapter: ProvinceAdapter
private lateinit var cityAdapter: CityAdapter private lateinit var cityAdapter: CityAdapter
private lateinit var subdistrictAdapter: SubdsitrictAdapter
private lateinit var bankAdapter: BankAdapter
// Request codes for file picking // Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001 private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002 private val PICK_KTP_REQUEST = 1002
private val PICK_NPWP_REQUEST = 1003 private val PICK_NPWP_REQUEST = 1003
private val PICK_NIB_REQUEST = 1004 private val PICK_NIB_REQUEST = 1004
private val PICK_PERSETUJUAN_REQUEST = 1005
private val PICK_QRIS_REQUEST = 1006
// Location request code // Location request code
private val LOCATION_PERMISSION_REQUEST = 2001 private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels { private val viewModel: RegisterStoreViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.Companion.getApiService(sessionManager)
val orderRepository = UserRepository(apiService) val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository) RegisterStoreViewModel(orderRepository)
} }
@ -84,8 +87,12 @@ class RegisterStoreActivity : AppCompatActivity() {
windowInsets windowInsets
} }
setupHeader()
provinceAdapter = ProvinceAdapter(this) provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this) cityAdapter = CityAdapter(this)
subdistrictAdapter = SubdsitrictAdapter(this)
bankAdapter = BankAdapter(this)
Log.d(TAG, "onCreate: Adapters initialized") Log.d(TAG, "onCreate: Adapters initialized")
setupDataBinding() setupDataBinding()
@ -99,8 +106,12 @@ class RegisterStoreActivity : AppCompatActivity() {
setupObservers() setupObservers()
Log.d(TAG, "onCreate: Observers setup completed") Log.d(TAG, "onCreate: Observers setup completed")
setupMap() viewModel.latitude.value = "-6.2088"
Log.d(TAG, "onCreate: Map setup completed") viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
setupDocumentUploads() setupDocumentUploads()
Log.d(TAG, "onCreate: Document uploads setup completed") Log.d(TAG, "onCreate: Document uploads setup completed")
@ -114,6 +125,9 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "onCreate: Fetching provinces from API") Log.d(TAG, "onCreate: Fetching provinces from API")
viewModel.getProvinces() viewModel.getProvinces()
viewModel.provinceId.observe(this) { validateRequiredFields() }
viewModel.cityId.observe(this) { validateRequiredFields() }
viewModel.storeTypeId.observe(this) { validateRequiredFields() }
// Setup register button // Setup register button
binding.btnRegister.setOnClickListener { binding.btnRegister.setOnClickListener {
@ -130,6 +144,46 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "onCreate: RegisterStoreActivity setup completed") Log.d(TAG, "onCreate: RegisterStoreActivity setup completed")
} }
private fun setupHeader() {
binding.header.main.background = ContextCompat.getColor(this, R.color.blue_500).toDrawable()
binding.header.headerTitle.visibility = View.GONE
binding.header.headerLeftIcon.setColorFilter(
ContextCompat.getColor(this, R.color.white),
android.graphics.PorterDuff.Mode.SRC_IN
)
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 &&
!viewModel.subdistrict.value.isNullOrBlank() &&
!viewModel.bankName.value.isNullOrBlank() &&
(viewModel.bankNumber.value ?: 0) > 0 &&
(viewModel.provinceId.value ?: 0) > 0 &&
!viewModel.cityId.value.isNullOrBlank() &&
(viewModel.storeTypeId.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank()
binding.btnRegister.isEnabled = true
if (isFormValid) {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
} else {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
}
}
private fun setupObservers() { private fun setupObservers() {
Log.d(TAG, "setupObservers: Setting up LiveData observers") Log.d(TAG, "setupObservers: Setting up LiveData observers")
@ -138,12 +192,12 @@ class RegisterStoreActivity : AppCompatActivity() {
when (state) { when (state) {
is Result.Loading -> { is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading provinces...") Log.d(TAG, "setupObservers: Loading provinces...")
binding.provinceProgressBar?.visibility = View.VISIBLE binding.provinceProgressBar.visibility = View.VISIBLE
binding.spinnerProvince.isEnabled = false binding.spinnerProvince.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces") Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar?.visibility = View.GONE binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true binding.spinnerProvince.isEnabled = true
// Update adapter with data // Update adapter with data
@ -151,7 +205,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
binding.provinceProgressBar?.visibility = View.GONE binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true binding.spinnerProvince.isEnabled = true
} }
} }
@ -162,12 +216,12 @@ class RegisterStoreActivity : AppCompatActivity() {
when (state) { when (state) {
is Result.Loading -> { is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading cities...") Log.d(TAG, "setupObservers: Loading cities...")
binding.cityProgressBar?.visibility = View.VISIBLE binding.cityProgressBar.visibility = View.VISIBLE
binding.spinnerCity.isEnabled = false binding.spinnerCity.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities") Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar?.visibility = View.GONE binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true binding.spinnerCity.isEnabled = true
// Update adapter with data // Update adapter with data
@ -175,7 +229,29 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
binding.cityProgressBar?.visibility = View.GONE binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true
}
}
}
viewModel.subdistrictState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupobservers: Loading Subdistrict...")
binding.subdistrictProgressBar.visibility = View.VISIBLE
binding.spinnerSubdistrict.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true
subdistrictAdapter.updateData(state.data)
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true binding.spinnerCity.isEnabled = true
} }
} }
@ -214,11 +290,11 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "setupStoreTypesObserver: Loading store types...") Log.d(TAG, "setupStoreTypesObserver: Loading store types...")
// Show loading indicator for store types spinner // Show loading indicator for store types spinner
binding.spinnerStoreType.isEnabled = false binding.spinnerStoreType.isEnabled = false
binding.storeTypeProgressBar?.visibility = View.VISIBLE binding.storeTypeProgressBar.visibility = View.VISIBLE
} else { } else {
Log.d(TAG, "setupStoreTypesObserver: Store types loading completed") Log.d(TAG, "setupStoreTypesObserver: Store types loading completed")
binding.spinnerStoreType.isEnabled = true binding.spinnerStoreType.isEnabled = true
binding.storeTypeProgressBar?.visibility = View.GONE binding.storeTypeProgressBar.visibility = View.GONE
} }
} }
@ -309,7 +385,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
// Hide progress bar after setup // Hide progress bar after setup
binding.storeTypeProgressBar?.visibility = View.GONE binding.storeTypeProgressBar.visibility = View.GONE
Log.d(TAG, "setupStoreTypeSpinner: Store type spinner setup completed") Log.d(TAG, "setupStoreTypeSpinner: Store type spinner setup completed")
} }
@ -351,6 +427,11 @@ class RegisterStoreActivity : AppCompatActivity() {
if (cityId != null) { if (cityId != null) {
Log.d(TAG, "Setting city ID: $cityId") Log.d(TAG, "Setting city ID: $cityId")
viewModel.cityId.value = cityId viewModel.cityId.value = cityId
Log.d(TAG, "Fetching subdistrict for city ID: $cityId")
viewModel.getSubdistrict(cityId)
subdistrictAdapter.clear()
binding.spinnerSubdistrict.setSelection(0)
viewModel.selectedCityId = cityId viewModel.selectedCityId = cityId
} else { } else {
Log.e(TAG, "Invalid city ID for position: $position") Log.e(TAG, "Invalid city ID for position: $position")
@ -362,6 +443,61 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
} }
//Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position")
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
if (subdistrictId != null) {
Log.d(TAG, "Setting subdistrict ID: $subdistrictId")
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No city selected")
}
}
binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Log.d(TAG, "Bank selected at position: $position")
val bankName = bankAdapter.getBankName(position)
if (bankName != null) {
Log.d(TAG, "Setting bank name: $bankName")
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
// Optional: Log the selected bank details
val selectedBank = bankAdapter.getBankItem(position)
selectedBank?.let {
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})")
}
// Hide progress bar if it was showing
binding.bankNameProgressBar.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
}
}
// Add initial hints to the spinners // Add initial hints to the spinners
if (provinceAdapter.isEmpty) { if (provinceAdapter.isEmpty) {
Log.d(TAG, "Adding default province hint") Log.d(TAG, "Adding default province hint")
@ -373,6 +509,16 @@ class RegisterStoreActivity : AppCompatActivity() {
cityAdapter.add("Pilih Kabupaten/Kota") cityAdapter.add("Pilih Kabupaten/Kota")
} }
if (subdistrictAdapter.isEmpty) {
Log.d(TAG, "Adding default kecamatan hint")
subdistrictAdapter.add("Pilih Kecamatan")
}
if (bankAdapter.isEmpty) {
Log.d(TAG, "Adding default bank hint")
bankAdapter.add("Pilih Bank")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed") Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
} }
@ -398,23 +544,11 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
// NPWP // NPWP
binding.containerNpwp?.setOnClickListener { binding.containerNpwp.setOnClickListener {
Log.d(TAG, "NPWP container clicked, picking image") Log.d(TAG, "NPWP container clicked, picking image")
pickImage(PICK_NPWP_REQUEST) pickImage(PICK_NPWP_REQUEST)
} }
// SPPIRT
binding.containerSppirt.setOnClickListener {
Log.d(TAG, "SPPIRT container clicked, picking document")
pickDocument(PICK_PERSETUJUAN_REQUEST)
}
// Halal
binding.containerHalal.setOnClickListener {
Log.d(TAG, "Halal container clicked, picking document")
pickDocument(PICK_QRIS_REQUEST)
}
Log.d(TAG, "setupDocumentUploads: Document upload buttons setup completed") Log.d(TAG, "setupDocumentUploads: Document upload buttons setup completed")
} }
@ -442,16 +576,16 @@ class RegisterStoreActivity : AppCompatActivity() {
handleCourierSelection("jne", isChecked) handleCourierSelection("jne", isChecked)
} }
binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNT checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
binding.checkboxPos.setOnCheckedChangeListener { _, isChecked -> binding.checkboxPos.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "POS checkbox ${if (isChecked) "checked" else "unchecked"}") Log.d(TAG, "POS checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("pos", isChecked) handleCourierSelection("pos", isChecked)
} }
binding.checkboxTiki.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "TIKI checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
Log.d(TAG, "setupCourierSelection: Courier checkboxes setup completed") Log.d(TAG, "setupCourierSelection: Courier checkboxes setup completed")
} }
@ -465,46 +599,47 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.selectedCouriers.remove(courier) viewModel.selectedCouriers.remove(courier)
Log.d(TAG, "handleCourierSelection: Removed courier: $courier. Current couriers: ${viewModel.selectedCouriers}") Log.d(TAG, "handleCourierSelection: Removed courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
} }
validateRequiredFields()
} }
private fun setupMap() { // private fun setupMap() {
Log.d(TAG, "setupMap: Setting up map container") // Log.d(TAG, "setupMap: Setting up map container")
// This would typically integrate with Google Maps SDK // // This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder // // For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener { // binding.mapContainer.setOnClickListener {
Log.d(TAG, "Map container clicked, checking location permission") // Log.d(TAG, "Map container clicked, checking location permission")
// Request location permission if not granted // // Request location permission if not granted
if (ContextCompat.checkSelfPermission( // if (ContextCompat.checkSelfPermission(
this, // this,
Manifest.permission.ACCESS_FINE_LOCATION // Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED // ) != PackageManager.PERMISSION_GRANTED
) { // ) {
Log.d(TAG, "Location permission not granted, requesting permission") // Log.d(TAG, "Location permission not granted, requesting permission")
ActivityCompat.requestPermissions( // ActivityCompat.requestPermissions(
this, // this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), // arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST // LOCATION_PERMISSION_REQUEST
) // )
viewModel.latitude.value = "-6.2088" // viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456" // viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location") // Log.d(TAG, "Location permission granted, setting default location")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}") // Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else { // } else {
Log.d(TAG, "Location permission already granted, setting location") // Log.d(TAG, "Location permission already granted, setting location")
// Show map selection UI // // Show map selection UI
// This would typically launch Maps UI for location selection // // This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates // // For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088" // viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456" // viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}") // Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} // }
} // }
//
Log.d(TAG, "setupMap: Map container setup completed") // Log.d(TAG, "setupMap: Map container setup completed")
} // }
private fun setupDataBinding() { private fun setupDataBinding() {
Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields") Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields")
@ -516,6 +651,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.storeName.value = s.toString() viewModel.storeName.value = s.toString()
Log.d(TAG, "Store name updated: ${s.toString()}") Log.d(TAG, "Store name updated: ${s.toString()}")
validateRequiredFields()
} }
}) })
@ -534,6 +670,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.street.value = s.toString() viewModel.street.value = s.toString()
Log.d(TAG, "Street address updated: ${s.toString()}") Log.d(TAG, "Street address updated: ${s.toString()}")
validateRequiredFields()
} }
}) })
@ -547,6 +684,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
// Handle invalid input // Handle invalid input
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e") Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
validateRequiredFields()
} }
} }
}) })
@ -578,25 +716,39 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.bankNumber.value = 0 // or 0 viewModel.bankNumber.value = 0 // or 0
Log.d(TAG, "Bank number set to default: 0") Log.d(TAG, "Bank number set to default: 0")
} }
validateRequiredFields()
} }
}) })
//
// binding.etSubdistrict.addTextChangedListener(object : TextWatcher {
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
// override fun afterTextChanged(s: Editable?) {
// viewModel.subdistrict.value = s.toString()
// Log.d(TAG, "Subdistrict updated: ${s.toString()}")
// validateRequiredFields()
// }
// })
binding.etSubdistrict.addTextChangedListener(object : TextWatcher { // binding.etBankName.addTextChangedListener(object: TextWatcher {
// override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
// override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
// override fun afterTextChanged(s: Editable?) {
// viewModel.bankName.value = s.toString()
// Log.d(TAG, "Bank name updated: ${s.toString()}")
// validateRequiredFields()
// }
// })
binding.etAccountName.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString() viewModel.accountName.value = s.toString()
Log.d(TAG, "Subdistrict updated: ${s.toString()}") Log.d(TAG, "Account Name updated: ${s.toString()}")
validateRequiredFields()
} }
})
binding.etBankName.addTextChangedListener(object: TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.bankName.value = s.toString()
Log.d(TAG, "Bank name updated: ${s.toString()}")
}
}) })
Log.d(TAG, "setupDataBinding: Text field data binding setup completed") Log.d(TAG, "setupDataBinding: Text field data binding setup completed")
@ -605,7 +757,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode") Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode")
if (resultCode == Activity.RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
val uri = data.data val uri = data.data
Log.d(TAG, "onActivityResult: URI received: $uri") Log.d(TAG, "onActivityResult: URI received: $uri")
when (requestCode) { when (requestCode) {
@ -618,26 +770,19 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "KTP image selected") Log.d(TAG, "KTP image selected")
viewModel.ktpUri = uri viewModel.ktpUri = uri
updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp) updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp)
validateRequiredFields()
} }
PICK_NPWP_REQUEST -> { PICK_NPWP_REQUEST -> {
Log.d(TAG, "NPWP document selected") Log.d(TAG, "NPWP document selected")
viewModel.npwpUri = uri viewModel.npwpUri = uri
updateDocumentPreview(binding.layoutUploadNpwp) updateDocumentPreview(binding.layoutUploadNpwp)
validateRequiredFields()
} }
PICK_NIB_REQUEST -> { PICK_NIB_REQUEST -> {
Log.d(TAG, "NIB document selected") Log.d(TAG, "NIB document selected")
viewModel.nibUri = uri viewModel.nibUri = uri
updateDocumentPreview(binding.layoutUploadNib) updateDocumentPreview(binding.layoutUploadNib)
} validateRequiredFields()
PICK_PERSETUJUAN_REQUEST -> {
Log.d(TAG, "SPPIRT document selected")
viewModel.persetujuanUri = uri
updateDocumentPreview(binding.layoutUploadSppirt)
}
PICK_QRIS_REQUEST -> {
Log.d(TAG, "Halal document selected")
viewModel.qrisUri = uri
updateDocumentPreview(binding.layoutUploadHalal)
} }
else -> { else -> {
Log.w(TAG, "Unknown request code: $requestCode") Log.w(TAG, "Unknown request code: $requestCode")

View File

@ -0,0 +1,25 @@
package com.alya.ecommerce_serang.ui.profile.mystore
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
import com.alya.ecommerce_serang.databinding.ActivityStoreOnReviewBinding
class StoreOnReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityStoreOnReviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStoreOnReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.header.headerTitle.text = "Verifikasi Pengajuan Toko"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
}

View File

@ -0,0 +1,26 @@
package com.alya.ecommerce_serang.ui.profile.mystore
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
import com.alya.ecommerce_serang.databinding.ActivityStoreSuspendedBinding
class StoreSuspendedActivity : AppCompatActivity() {
private lateinit var binding: ActivityStoreSuspendedBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStoreSuspendedBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.header.headerTitle.text = "Toko Dinonaktifkan"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
}

View File

@ -15,11 +15,15 @@ import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -38,6 +42,8 @@ class BalanceTopUpActivity : AppCompatActivity() {
private lateinit var spinnerPaymentMethod: Spinner private lateinit var spinnerPaymentMethod: Spinner
private lateinit var edtTransactionDate: EditText private lateinit var edtTransactionDate: EditText
private lateinit var datePickerIcon: ImageView private lateinit var datePickerIcon: ImageView
private lateinit var layoutMBankingInstructions: View
private lateinit var layoutATMInstructions: View
private lateinit var btnSend: Button private lateinit var btnSend: Button
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
@ -52,7 +58,22 @@ class BalanceTopUpActivity : AppCompatActivity() {
val imageUri = result.data?.data val imageUri = result.data?.data
imageUri?.let { imageUri?.let {
selectedImageUri = it selectedImageUri = it
imgPreview.setImageURI(it)
// Compress the image before displaying it
val compressedFile = compressImage(
context = this,
uri = it,
filename = "topup_img",
maxWidth = 1024,
maxHeight = 1024,
quality = 80
)
// Display the compressed image
selectedImageUri = Uri.fromFile(compressedFile)
imgPreview.setImageURI(Uri.fromFile(compressedFile))
validateForm()
} }
} }
} }
@ -71,6 +92,8 @@ class BalanceTopUpActivity : AppCompatActivity() {
spinnerPaymentMethod = findViewById(R.id.spinner_metode_bayar) spinnerPaymentMethod = findViewById(R.id.spinner_metode_bayar)
edtTransactionDate = findViewById(R.id.edt_tgl_transaksi) edtTransactionDate = findViewById(R.id.edt_tgl_transaksi)
datePickerIcon = findViewById(R.id.img_date_picker) datePickerIcon = findViewById(R.id.img_date_picker)
layoutMBankingInstructions = findViewById(R.id.layout_mbanking_instructions)
layoutATMInstructions = findViewById(R.id.layout_atm_instructions)
btnSend = findViewById(R.id.btn_send) btnSend = findViewById(R.id.btn_send)
// Setup header title // Setup header title
@ -98,10 +121,27 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Fetch payment methods // Fetch payment methods
fetchPaymentMethods() fetchPaymentMethods()
setupClickListeners("1234567890")
// Setup submit button // Setup submit button
btnSend.setOnClickListener { btnSend.setOnClickListener {
submitForm() submitForm()
} }
// Validate form when any input changes
edtNominal.doAfterTextChanged { validateForm() }
edtTransactionDate.doAfterTextChanged { validateForm() }
spinnerPaymentMethod.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedPaymentId = paymentMethods[position].id
validateForm()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
selectedPaymentId = -1
validateForm()
}
}
} }
private fun openGallery() { private fun openGallery() {
@ -200,6 +240,24 @@ class BalanceTopUpActivity : AppCompatActivity() {
} }
} }
private fun validateForm() {
val isNominalFilled = edtNominal.text.toString().trim().isNotEmpty()
val isPaymentMethodSelected = selectedPaymentId != -1
val isTransactionDateFilled = edtTransactionDate.text.toString().trim().isNotEmpty()
val isImageSelected = selectedImageUri != null
val valid = isNominalFilled && isPaymentMethodSelected && isTransactionDateFilled && isImageSelected
btnSend.isEnabled = valid
btnSend.setTextColor(
if (valid) ContextCompat.getColor(this, R.color.white)
else ContextCompat.getColor(this, R.color.black_300)
)
btnSend.setBackgroundResource(
if (valid) R.drawable.bg_button_active
else R.drawable.bg_button_disabled
)
}
private fun submitForm() { private fun submitForm() {
// Prevent multiple clicks // Prevent multiple clicks
if (!btnSend.isEnabled) { if (!btnSend.isEnabled) {
@ -316,7 +374,7 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the success message // Show a dialog with the success message
runOnUiThread { runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity) AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Berhasil") .setTitle("Berhasil")
.setMessage(successMessage) .setMessage(successMessage)
.setPositiveButton("OK") { dialog, _ -> .setPositiveButton("OK") { dialog, _ ->
@ -350,7 +408,7 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the error message // Show a dialog with the error message
runOnUiThread { runOnUiThread {
androidx.appcompat.app.AlertDialog.Builder(this@BalanceTopUpActivity) AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Error Response") .setTitle("Error Response")
.setMessage(errorMessage) .setMessage(errorMessage)
.setPositiveButton("OK") { dialog, _ -> .setPositiveButton("OK") { dialog, _ ->
@ -392,4 +450,46 @@ class BalanceTopUpActivity : AppCompatActivity() {
return tempFile return tempFile
} }
private fun setupClickListeners(bankAccountNumber: String) {
// Instructions clicks
layoutMBankingInstructions.setOnClickListener {
showInstructions("mBanking", bankAccountNumber)
}
layoutATMInstructions.setOnClickListener {
showInstructions("ATM", bankAccountNumber)
}
}
private fun showInstructions(type: String, bankAccountNumber: String) {
// Implementasi tampilkan instruksi
val instructions = when (type) {
"mBanking" -> listOf(
"1. Login ke aplikasi mobile banking",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan nomor rekening tujuan: $bankAccountNumber",
"5. Masukkan nominal saldo yang ingin diisi",
"6. Konfirmasi dan selesaikan transfer"
)
"ATM" -> listOf(
"1. Masukkan kartu ATM dan PIN",
"2. Pilih menu Transfer",
"3. Pilih menu Antar Rekening",
"4. Masukkan kode bank dan nomor rekening tujuan: $bankAccountNumber",
"5. Masukkan nominal saldo yang ingin diisi",
"6. Konfirmasi dan selesaikan transfer"
)
else -> emptyList()
}
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null)
.setPositiveButton("Tutup", null)
.create()
dialog.show()
}
} }

View File

@ -25,6 +25,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
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
@ -373,7 +374,8 @@ class ChatStoreActivity : AppCompatActivity() {
} }
}) })
viewModel.state.observe(this, Observer { state -> lifecycleScope.launchWhenStarted {
viewModel.state.collect { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}") Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages // Update messages
@ -434,7 +436,8 @@ class ChatStoreActivity : AppCompatActivity() {
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show() Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError() viewModel.clearError()
} }
}) }
}
} }
private fun showOptionsMenu() { private fun showOptionsMenu() {

View File

@ -5,56 +5,47 @@ import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.FragmentChangePriceBottomSheetBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
// TODO: Rename parameter arguments, choose names that match class ChangePriceBottomSheetFragment(
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private val product: ProductsItem,
private const val ARG_PARAM1 = "param1" private val onSave: (productId: Int, newPrice: Int) -> Unit
private const val ARG_PARAM2 = "param2" ) : BottomSheetDialogFragment() {
private var _binding: FragmentChangePriceBottomSheetBinding? = null
/** private val binding get() = _binding!!
* A simple [Fragment] subclass.
* Use the [ChangePriceBottomSheetFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class ChangePriceBottomSheetFragment : Fragment() {
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
// Inflate the layout for this fragment _binding = FragmentChangePriceBottomSheetBinding.inflate(inflater, container, false)
return inflater.inflate(R.layout.fragment_change_price_bottom_sheet, container, false)
binding.header.headerTitle.text = "Atur Harga"
binding.header.headerLeftIcon.setImageResource(R.drawable.ic_close)
binding.header.headerLeftIcon.setOnClickListener { dismiss() }
binding.edtPrice.setText(product.price)
binding.btnSave.setOnClickListener {
val newPrice = binding.edtPrice.text.toString().replace(".", "").toIntOrNull()
if (newPrice != null && newPrice > 0) {
onSave(product.id, newPrice)
dismiss()
} else {
Toast.makeText(requireContext(), "Masukkan harga yang valid", Toast.LENGTH_SHORT).show()
}
}
return binding.root
} }
companion object { override fun onDestroyView() {
/** super.onDestroyView()
* Use this factory method to create a new instance of _binding = null
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment ChangePriceBottomSheetFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
ChangePriceBottomSheetFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
} }
} }

View File

@ -0,0 +1,69 @@
package com.alya.ecommerce_serang.ui.profile.mystore.product
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.FragmentChangeStockBottomSheetBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class ChangeStockBottomSheetFragment(
private val product: ProductsItem,
private val onSave: (productId: Int, newStock: Int) -> Unit
): BottomSheetDialogFragment() {
private var _binding: FragmentChangeStockBottomSheetBinding? = null
private val binding get() = _binding!!
private var stock = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentChangeStockBottomSheetBinding.inflate(inflater, container, false)
binding.header.headerTitle.text = "Atur Stok"
binding.header.headerLeftIcon.setImageResource(R.drawable.ic_close)
binding.header.headerLeftIcon.setOnClickListener { dismiss() }
stock = product.stock
updateStock()
binding.btnMinus.setOnClickListener {
if (stock > 0) stock--
updateStock()
}
binding.btnPlus.setOnClickListener {
stock++
updateStock()
}
binding.btnSave.setOnClickListener {
onSave(product.id, stock)
dismiss()
}
return binding.root
}
private fun updateStock() {
binding.edtStock.setText(stock.toString())
if (stock == 0) {
binding.btnMinus.isEnabled = false
binding.btnMinus.setColorFilter(ContextCompat.getColor(requireContext(), R.color.black_100))
} else {
binding.btnMinus.isEnabled = true
binding.btnMinus.setColorFilter(ContextCompat.getColor(requireContext(), R.color.blue_500))
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -12,36 +12,37 @@ import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.alya.ecommerce_serang.R
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
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.Preorder import com.alya.ecommerce_serang.data.api.dto.Preorder
import com.alya.ecommerce_serang.data.api.dto.Wholesale
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.FileUtils.compressFile
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
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 okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.getValue
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.data.api.dto.Wholesale
class DetailStoreProductActivity : AppCompatActivity() { class DetailStoreProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailStoreProductBinding private lateinit var binding: ActivityDetailStoreProductBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private lateinit var categoryList: List<CategoryItem> private var categoryList: List<CategoryItem> = emptyList()
private var imageUri: Uri? = null private var imageUri: Uri? = null
private var sppirtUri: Uri? = null private var sppirtUri: Uri? = null
private var halalUri: Uri? = null private var halalUri: Uri? = null
@ -61,7 +62,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
imageUri = result.data?.data imageUri = result.data?.data
imageUri?.let { imageUri?.let {
binding.ivPreviewFoto.setImageURI(it) compressImage(this, it, "productimg").let { compressedImageFile ->
binding.ivPreviewFoto.setImageURI(Uri.fromFile(compressedImageFile))
imageUri = Uri.fromFile(compressedImageFile)
}
binding.switcherFotoProduk.showNext() binding.switcherFotoProduk.showNext()
hasImage = true hasImage = true
} }
@ -71,17 +75,21 @@ class DetailStoreProductActivity : AppCompatActivity() {
private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) { if (uri != null && isValidFile(uri)) {
sppirtUri = uri compressFile(this, uri).let { compressedFile ->
binding.tvSppirtName.text = getFileName(uri) sppirtUri = compressedFile?.toUri()
binding.switcherSppirt.showNext() binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext()
}
} }
} }
private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) { if (uri != null && isValidFile(uri)) {
halalUri = uri compressFile(this, uri).let { compressedFile ->
binding.tvHalalName.text = getFileName(uri) halalUri = compressedFile?.toUri()
binding.switcherHalal.showNext() binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext()
}
} }
} }
@ -93,7 +101,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
val isEditing = intent.getBooleanExtra("is_editing", false) val isEditing = intent.getBooleanExtra("is_editing", false)
productId = intent.getIntExtra("product_id", -1) productId = intent.getIntExtra("product_id", -1)
binding.header.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk" binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
if (isEditing && productId != null && productId != -1) { if (isEditing && productId != null && productId != -1) {
viewModel.loadProductDetail(productId!!) viewModel.loadProductDetail(productId!!)
@ -140,7 +148,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
} }
} }
binding.header.headerLeftIcon.setOnClickListener { binding.headerStoreProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
} }

View File

@ -11,18 +11,19 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityProductBinding import com.alya.ecommerce_serang.databinding.ActivityProductBinding
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
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 ProductActivity : AppCompatActivity() { class ProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityProductBinding private lateinit var binding: ActivityProductBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private lateinit var productAdapter: ProductAdapter
private val viewModel: ProductViewModel by viewModels { private val viewModel: ProductViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
sessionManager = SessionManager(this)
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService) val productRepository = ProductRepository(apiService)
ProductViewModel(productRepository) ProductViewModel(productRepository)
@ -30,6 +31,8 @@ class ProductActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
sessionManager = SessionManager(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityProductBinding.inflate(layoutInflater) binding = ActivityProductBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -56,9 +59,18 @@ class ProductActivity : AppCompatActivity() {
is Result.Success -> { is Result.Success -> {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
val products = result.data val products = result.data
binding.rvStoreProduct.adapter = ProductAdapter(products) {
Toast.makeText(this, "Produk: ${it.name}", Toast.LENGTH_SHORT).show() productAdapter = ProductAdapter(
} products,
onItemClick = { products ->
Toast.makeText(this, "Produk ${products.name} diklik", Toast.LENGTH_SHORT).show()
},
onUpdateProduct = { productId, updatedFields ->
viewModel.updateProduct(productId, updatedFields)
}
)
binding.rvStoreProduct.adapter = productAdapter
} }
is Result.Error -> { is Result.Error -> {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
@ -66,17 +78,30 @@ class ProductActivity : AppCompatActivity() {
} }
} }
} }
viewModel.productUpdateResult.observe(this) { result ->
when (result) {
is Result.Success -> {
Toast.makeText(this, "Produk berhasil diperbarui", Toast.LENGTH_SHORT).show()
viewModel.loadMyStoreProducts()
}
is Result.Error -> {
Toast.makeText(this, "Gagal memperbarui produk", Toast.LENGTH_SHORT).show()
}
else -> {}
}
}
} }
private fun setupHeader() { private fun setupHeader() {
binding.header.headerTitle.text = "Produk Saya" binding.headerListProduct.headerTitle.text = "Produk Saya"
binding.header.headerRightText.visibility = View.VISIBLE binding.headerListProduct.headerRightText.visibility = View.VISIBLE
binding.header.headerLeftIcon.setOnClickListener { binding.headerListProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
binding.header.headerRightText.setOnClickListener { binding.headerListProduct.headerRightText.setOnClickListener {
val intent = Intent(this, DetailStoreProductActivity::class.java) val intent = Intent(this, DetailStoreProductActivity::class.java)
intent.putExtra("is_editing", false) intent.putExtra("is_editing", false)
startActivity(intent) startActivity(intent)
@ -86,4 +111,6 @@ class ProductActivity : AppCompatActivity() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
binding.rvStoreProduct.layoutManager = LinearLayoutManager(this) binding.rvStoreProduct.layoutManager = LinearLayoutManager(this)
} }
} }

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.profile.mystore.product
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
@ -15,10 +16,14 @@ import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.Product import com.alya.ecommerce_serang.data.api.dto.Product
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
class ProductAdapter( class ProductAdapter(
private val products: List<ProductsItem>, private val products: List<ProductsItem>,
private val onItemClick: (ProductsItem) -> Unit private val onItemClick: (ProductsItem) -> Unit,
private val onUpdateProduct: (productId: Int, updatedFields: Map<String, RequestBody>) -> Unit
) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() { ) : RecyclerView.Adapter<ProductAdapter.ProductViewHolder>() {
inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@ -28,6 +33,8 @@ class ProductAdapter(
private val tvProductStock: TextView = itemView.findViewById(R.id.tv_product_stock) private val tvProductStock: TextView = itemView.findViewById(R.id.tv_product_stock)
private val tvProductStatus: TextView = itemView.findViewById(R.id.tv_product_status) private val tvProductStatus: TextView = itemView.findViewById(R.id.tv_product_status)
private val ivMenu: ImageView = itemView.findViewById(R.id.iv_menu) private val ivMenu: ImageView = itemView.findViewById(R.id.iv_menu)
private val btnChangePrice: Button = itemView.findViewById(R.id.btn_change_price)
private val btnChangeStock: Button = itemView.findViewById(R.id.btn_change_stock)
fun bind(product: ProductsItem) { fun bind(product: ProductsItem) {
tvProductName.text = product.name tvProductName.text = product.name
@ -55,7 +62,6 @@ class ProductAdapter(
.into(ivProduct) .into(ivProduct)
ivMenu.setOnClickListener { ivMenu.setOnClickListener {
// Show Bottom Sheet when menu is clicked
val bottomSheetFragment = ProductOptionsBottomSheetFragment(product) val bottomSheetFragment = ProductOptionsBottomSheetFragment(product)
bottomSheetFragment.show( bottomSheetFragment.show(
(itemView.context as FragmentActivity).supportFragmentManager, (itemView.context as FragmentActivity).supportFragmentManager,
@ -63,6 +69,36 @@ class ProductAdapter(
) )
} }
btnChangePrice.setOnClickListener {
val bottomSheetFragment = ChangePriceBottomSheetFragment(product) { id, newPrice ->
val body = mapOf(
"product_id" to id.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
"price" to newPrice.toString().toRequestBody("text/plain".toMediaTypeOrNull())
)
onUpdateProduct(id, body)
Toast.makeText(itemView.context, "Harga berhasil diubah", Toast.LENGTH_SHORT).show()
}
bottomSheetFragment.show(
(itemView.context as FragmentActivity).supportFragmentManager,
bottomSheetFragment.tag
)
}
btnChangeStock.setOnClickListener {
val bottomSheetFragment = ChangeStockBottomSheetFragment(product) { id, newStock ->
val body = mapOf(
"product_id" to id.toString().toRequestBody("text/plain".toMediaTypeOrNull()),
"stock" to newStock.toString().toRequestBody("text/plain".toMediaTypeOrNull())
)
onUpdateProduct(id, body)
Toast.makeText(itemView.context, "Stok berhasil diubah", Toast.LENGTH_SHORT).show()
}
bottomSheetFragment.show(
(itemView.context as FragmentActivity).supportFragmentManager,
bottomSheetFragment.tag
)
}
itemView.setOnClickListener { itemView.setOnClickListener {
onItemClick(product) onItemClick(product)
} }

View File

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.ui.profile.mystore.profile.address package com.alya.ecommerce_serang.ui.profile.mystore.profile.address
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -14,13 +13,16 @@ import com.alya.ecommerce_serang.BuildConfig
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.City import com.alya.ecommerce_serang.data.api.dto.City
import com.alya.ecommerce_serang.data.api.dto.Province import com.alya.ecommerce_serang.data.api.dto.Province
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesItem
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.AddressRepository import com.alya.ecommerce_serang.data.repository.AddressRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
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.AddressViewModel
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
class DetailStoreAddressActivity : AppCompatActivity() { class DetailStoreAddressActivity : AppCompatActivity() {
@ -30,10 +32,15 @@ class DetailStoreAddressActivity : AppCompatActivity() {
private var selectedProvinceId: String? = null private var selectedProvinceId: String? = null
private var selectedCityId: String? = null private var selectedCityId: String? = null
private var selectedSubdistrict: String? = null
private var provinces: List<Province> = emptyList() private var provinces: List<Province> = emptyList()
private var cities: List<City> = emptyList() private var cities: List<City> = emptyList()
private var subdistrict: List<SubdistrictsItem> = emptyList()
private var currentAddress: AddressesItem? = null
private val TAG = "StoreAddressActivity" // private lateinit var subdistrictAdapter: SubdsitrictAdapter
private val TAG = "DetailStoreAddressActivity"
private val viewModel: AddressViewModel by viewModels { private val viewModel: AddressViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
@ -58,13 +65,15 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding.tvError.visibility = View.GONE binding.tvError.visibility = View.GONE
// Set up header title // Set up header title
binding.header.headerTitle.text = "Atur Alamat Toko" binding.headerAddressStore.headerTitle.text = "Atur Alamat Toko"
// Set up back button // Set up back button
binding.header.headerLeftIcon.setOnClickListener { binding.headerAddressStore.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
// subdistrictAdapter = SubdsitrictAdapter(this)
setupSpinners() setupSpinners()
setupObservers() setupObservers()
setupSaveButton() setupSaveButton()
@ -113,11 +122,26 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedCityId = if (position > 0) cities[position - 1].cityId else null selectedCityId = if (position > 0) cities[position - 1].cityId else null
viewModel.getSubdistrict(selectedCityId.toString())
checkAllFieldsFilled() checkAllFieldsFilled()
} }
override fun onNothingSelected(p0: AdapterView<*>?) {} override fun onNothingSelected(p0: AdapterView<*>?) {}
} }
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
selectedSubdistrict = if (position > 0) subdistrict[position - 1].subdistrictName else null
checkAllFieldsFilled()
}
override fun onNothingSelected(p0: AdapterView<*>?) {}
}
} }
private fun setupObservers() { private fun setupObservers() {
@ -176,19 +200,58 @@ class DetailStoreAddressActivity : AppCompatActivity() {
} }
} }
viewModel.subdistrictState.observe(this) { result ->
when (result) {
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
showSubLoading(true)
}
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
showSubLoading(false)
subdistrict = result.data
val subdistrictNames = mutableListOf("Pilih Kecamatan")
subdistrictNames.addAll(result.data.map { it.subdistrictName })
val adapter = ArrayAdapter(
this,
android.R.layout.simple_spinner_item,
subdistrictNames
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinnerSubdistrict.adapter = adapter
// Compare by name, since stored value is the subdistrict name
viewModel.storeAddress.value?.let { address ->
val index = subdistrict.indexOfFirst { it.subdistrictName == address.subdistrict }
if (index != -1) {
binding.spinnerSubdistrict.setSelection(index + 1)
}
}
}
is Result.Error -> {
showSubLoading(false)
Log.e(TAG, "Error: ${result.exception.message}", result.exception)
}
}
}
// Observe store address data // Observe store address data
viewModel.storeAddress.observe(this) { address -> viewModel.storeAddress.observe(this) { address ->
currentAddress = address
Log.d(TAG, "Received store address: $address") Log.d(TAG, "Received store address: $address")
address?.let { address?.let {
// Set the fields // Set the fields
binding.edtStreet.setText(it.street) binding.edtStreet.setText(it.street)
binding.edtSubdistrict.setText(it.subdistrict) // binding.edtSubdistrict.setText(it.subdistrict)
binding.edtDetailAddress.setText(it.detail ?: "") binding.edtDetailAddress.setText(it.detail ?: "")
binding.edtPostalCode.setText(it.postalCode) binding.edtPostalCode.setText(it.postalCode)
binding.edtLatitude.setText(it.latitude.toString()) binding.edtLatitude.setText(it.latitude.toString())
binding.edtLongitude.setText(it.longitude.toString()) binding.edtLongitude.setText(it.longitude.toString())
selectedProvinceId = it.provinceId selectedProvinceId = it.provinceId
selectedCityId = it.cityId selectedCityId = it.cityId
selectedSubdistrict = it.subdistrict
// Find province index and select it after provinces are loaded // Find province index and select it after provinces are loaded
if (provinces.isNotEmpty()) { if (provinces.isNotEmpty()) {
@ -214,11 +277,12 @@ class DetailStoreAddressActivity : AppCompatActivity() {
} }
// Observe save success // Observe save success
viewModel.saveSuccess.observe(this) { viewModel.saveSuccess.observe(this) { success ->
if (it) { if (success) {
Toast.makeText(this, "Alamat berhasil disimpan", Toast.LENGTH_SHORT).show() Log.d(TAG, "Address updated successfully")
setResult(Activity.RESULT_OK)
finish() finish()
} else {
Log.e(TAG, "Failed to update address")
} }
} }
} }
@ -226,14 +290,15 @@ class DetailStoreAddressActivity : AppCompatActivity() {
private fun setupSaveButton() { private fun setupSaveButton() {
binding.btnSaveAddress.setOnClickListener { binding.btnSaveAddress.setOnClickListener {
val street = binding.edtStreet.text.toString() val street = binding.edtStreet.text.toString()
val subdistrict = binding.edtSubdistrict.text.toString()
val detail = binding.edtDetailAddress.text.toString() val detail = binding.edtDetailAddress.text.toString()
val postalCode = binding.edtPostalCode.text.toString() val postalCode = binding.edtPostalCode.text.toString()
val latitude = binding.edtLatitude.text.toString().toDoubleOrNull() ?: 0.0 val latitude = binding.edtLatitude.text.toString()
val longitude = binding.edtLongitude.text.toString().toDoubleOrNull() ?: 0.0 val longitude = binding.edtLongitude.text.toString()
val city = cities.find { it.cityId == selectedCityId } val city = cities.find { it.cityId == selectedCityId }
val province = provinces.find { it.provinceId == selectedProvinceId } val province = provinces.find { it.provinceId == selectedProvinceId }
val subdistrictName = subdistrict.find { it.subdistrictName == selectedSubdistrict }?.subdistrictName.toString()
Log.d(TAG, "Subdistrict name: $subdistrictName")
// Validate required fields // Validate required fields
if (selectedProvinceId.isNullOrEmpty() || city == null || street.isEmpty() || subdistrict.isEmpty() || postalCode.isEmpty()) { if (selectedProvinceId.isNullOrEmpty() || city == null || street.isEmpty() || subdistrict.isEmpty() || postalCode.isEmpty()) {
@ -241,19 +306,35 @@ class DetailStoreAddressActivity : AppCompatActivity() {
return@setOnClickListener return@setOnClickListener
} }
// Save address val oldAddress = currentAddress ?: return@setOnClickListener
viewModel.saveStoreAddress( val newAddress = oldAddress.copy(
provinceId = selectedProvinceId!!, provinceId = selectedProvinceId!!,
provinceName = province?.provinceName ?: "",
cityId = city.cityId, cityId = city.cityId,
cityName = city.cityName,
street = street, street = street,
subdistrict = subdistrict, subdistrict = subdistrictName,
detail = detail, detail = detail,
postalCode = postalCode, postalCode = postalCode,
latitude = latitude, latitude = latitude,
longitude = longitude longitude = longitude,
phone = oldAddress.phone,
recipient = oldAddress.recipient ?: "",
isStoreLocation = oldAddress.isStoreLocation,
villageId = oldAddress.villageId
) )
viewModel.saveStoreAddress(oldAddress, newAddress)
// Save address
// viewModel.saveStoreAddress(
// provinceId = selectedProvinceId!!,
// provinceName = province?.provinceName ?: "",
// cityId = city.cityId,
// cityName = city.cityName,
// street = street,
// subdistrict = subdistrict,
// detail = detail,
// postalCode = postalCode,
// latitude = latitude,
// longitude = longitude
// )
} }
} }
@ -264,15 +345,14 @@ class DetailStoreAddressActivity : AppCompatActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
} }
binding.edtStreet.addTextChangedListener(watcher) binding.edtStreet.addTextChangedListener(watcher)
binding.edtSubdistrict.addTextChangedListener(watcher)
binding.edtPostalCode.addTextChangedListener(watcher) binding.edtPostalCode.addTextChangedListener(watcher)
} }
private fun checkAllFieldsFilled() { private fun checkAllFieldsFilled() {
val allValid = !selectedProvinceId.isNullOrEmpty() val allValid = !selectedProvinceId.isNullOrEmpty()
&& !selectedCityId.isNullOrEmpty() && !selectedCityId.isNullOrEmpty()
&& !selectedSubdistrict.isNullOrEmpty()
&& binding.edtStreet.text.isNotBlank() && binding.edtStreet.text.isNotBlank()
&& binding.edtSubdistrict.text.isNotBlank()
&& binding.edtPostalCode.text.isNotBlank() && binding.edtPostalCode.text.isNotBlank()
binding.btnSaveAddress.let { binding.btnSaveAddress.let {
@ -284,6 +364,7 @@ class DetailStoreAddressActivity : AppCompatActivity() {
it.isEnabled = false it.isEnabled = false
it.setBackgroundResource(R.drawable.bg_button_disabled) it.setBackgroundResource(R.drawable.bg_button_disabled)
it.setTextColor(getColor(R.color.black_300)) it.setTextColor(getColor(R.color.black_300))
Toast.makeText(this, "Periksa dan lenkapi alamat anda", Toast.LENGTH_SHORT).show()
} }
} }
} }
@ -298,6 +379,12 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding.spinnerCity.visibility = if (isLoading) View.GONE else View.VISIBLE binding.spinnerCity.visibility = if (isLoading) View.GONE else View.VISIBLE
} }
private fun showSubLoading(isLoading: Boolean) {
binding.subdistrictProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.spinnerSubdistrict.visibility = if (isLoading) View.GONE else View.VISIBLE
}
private fun showError(message: String) { private fun showError(message: String) {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.tvError.visibility = View.VISIBLE binding.tvError.visibility = View.VISIBLE

View File

@ -6,9 +6,12 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.AdapterView
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Spinner
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -18,6 +21,7 @@ import com.alya.ecommerce_serang.data.api.dto.PaymentInfo
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.PaymentInfoRepository import com.alya.ecommerce_serang.data.repository.PaymentInfoRepository
import com.alya.ecommerce_serang.databinding.ActivityPaymentInfoBinding import com.alya.ecommerce_serang.databinding.ActivityPaymentInfoBinding
import com.alya.ecommerce_serang.ui.order.address.BankAdapter
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.UriToFileConverter import com.alya.ecommerce_serang.utils.UriToFileConverter
@ -32,6 +36,7 @@ class PaymentInfoActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var selectedQrisImageUri: Uri? = null private var selectedQrisImageUri: Uri? = null
private var selectedQrisImageFile: File? = null private var selectedQrisImageFile: File? = null
private lateinit var bankAdapter: BankAdapter
// Store form data between dialog reopenings // Store form data between dialog reopenings
private var savedBankName: String = "" private var savedBankName: String = ""
@ -95,6 +100,7 @@ class PaymentInfoActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
bankAdapter = BankAdapter(this)
setupRecyclerView() setupRecyclerView()
setupObservers() setupObservers()
@ -173,10 +179,47 @@ class PaymentInfoActivity : AppCompatActivity() {
builder.setView(dialogView) builder.setView(dialogView)
val dialog = builder.create() val dialog = builder.create()
val spinnerBankName = dialogView.findViewById<Spinner>(R.id.spinner_bank_name)
val progressBarBank = dialogView.findViewById<ProgressBar>(R.id.bank_name_progress_bar)
spinnerBankName.adapter = bankAdapter
spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Log.d(TAG, "Bank selected at position: $position")
val bankName = bankAdapter.getBankName(position)
if (bankName != null) {
Log.d(TAG, "Setting bank name: $bankName")
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
// Optional: Log the selected bank details
val selectedBank = bankAdapter.getBankItem(position)
selectedBank?.let {
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})")
}
// Hide progress bar if it was showing
progressBarBank.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
}
}
// Get references to views in the dialog // Get references to views in the dialog
val btnAddQris = dialogView.findViewById<Button>(R.id.btn_add_qris) val btnAddQris = dialogView.findViewById<Button>(R.id.btn_add_qris)
val bankNameEditText = dialogView.findViewById<EditText>(R.id.edt_bank_name) // val spinnerBankName = dialogView.findViewById<Spinner>(R.id.spinner_bank_name)
val bankNumberEditText = dialogView.findViewById<EditText>(R.id.edt_bank_number) val bankNumberEditText = dialogView.findViewById<EditText>(R.id.edt_bank_number)
val accountNameEditText = dialogView.findViewById<EditText>(R.id.edt_account_name) val accountNameEditText = dialogView.findViewById<EditText>(R.id.edt_account_name)
val qrisPreview = dialogView.findViewById<ImageView>(R.id.iv_qris_preview) val qrisPreview = dialogView.findViewById<ImageView>(R.id.iv_qris_preview)
@ -185,7 +228,10 @@ class PaymentInfoActivity : AppCompatActivity() {
// When reopening, restore the previously entered values // When reopening, restore the previously entered values
if (isReopened) { if (isReopened) {
bankNameEditText.setText(savedBankName) val savedPosition = bankAdapter.findPositionByName(savedBankName)
if (savedPosition >= 0) {
spinnerBankName.setSelection(savedPosition)
}
bankNumberEditText.setText(savedBankNumber) bankNumberEditText.setText(savedBankNumber)
accountNameEditText.setText(savedAccountName) accountNameEditText.setText(savedAccountName)
@ -199,7 +245,7 @@ class PaymentInfoActivity : AppCompatActivity() {
btnAddQris.setOnClickListener { btnAddQris.setOnClickListener {
// Save the current values before dismissing // Save the current values before dismissing
savedBankName = bankNameEditText.text.toString().trim() savedBankName = viewModel.selectedBankName ?: ""
savedBankNumber = bankNumberEditText.text.toString().trim() savedBankNumber = bankNumberEditText.text.toString().trim()
savedAccountName = accountNameEditText.text.toString().trim() savedAccountName = accountNameEditText.text.toString().trim()
@ -212,13 +258,13 @@ class PaymentInfoActivity : AppCompatActivity() {
} }
btnSave.setOnClickListener { btnSave.setOnClickListener {
val bankName = bankNameEditText.text.toString().trim() val bankName = viewModel.selectedBankName ?: ""
val bankNumber = bankNumberEditText.text.toString().trim() val bankNumber = bankNumberEditText.text.toString().trim()
val accountName = accountNameEditText.text.toString().trim() val accountName = accountNameEditText.text.toString().trim()
// Validation // Validation
if (bankName.isEmpty()) { if (bankName.isEmpty()) {
showSnackbar("Nama bank tidak boleh kosong") showSnackbar("Pilih nama bank terlebih dahulu")
return@setOnClickListener return@setOnClickListener
} }

View File

@ -0,0 +1,74 @@
package com.alya.ecommerce_serang.ui.profile.mystore.review
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.commit
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ReviewRepository
import com.alya.ecommerce_serang.databinding.ActivityReviewBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel
class ReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewBinding
private lateinit var sessionManager: SessionManager
private val viewModel: ReviewViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val reviewRepository = ReviewRepository(apiService)
ReviewViewModel(reviewRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
setupHeader()
viewModel.getReview("all")
viewModel.averageScore.observe(this) { binding.tvReviewScore.text = it }
viewModel.totalReview.observe(this) { binding.tvTotalReview.text = "$it rating" }
viewModel.totalReviewWithDesc.observe(this) { binding.tvTotalReviewWithDesc.text = "$it ulasan" }
if (savedInstanceState == null) {
showReviewFragment()
}
}
private fun setupHeader() {
binding.header.headerTitle.text = "Ulasan Pembeli"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressed()
finish()
}
}
private fun showReviewFragment() {
supportFragmentManager.commit {
replace(R.id.fragment_container_reviews, ReviewFragment())
}
}
}

View File

@ -0,0 +1,81 @@
package com.alya.ecommerce_serang.ui.profile.mystore.review
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ReviewsItem
import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ReviewAdapter(
private val viewModel: ReviewViewModel
): RecyclerView.Adapter<ReviewAdapter.ReviewViewHolder>() {
private val reviews = mutableListOf<ReviewsItem>()
private var fragmentScore: String = "all"
fun setFragmentScore(score: String) {
fragmentScore = score
}
fun submitList(newReviews: List<ReviewsItem>) {
reviews.clear()
reviews.addAll(newReviews)
notifyDataSetChanged()
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ReviewAdapter.ReviewViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_store_product_review, parent, false)
return ReviewViewHolder(view)
}
override fun onBindViewHolder(holder: ReviewViewHolder, position: Int) {
if (position < reviews.size) {
holder.bind(reviews[position])
} else {
Log.e("ReviewAdapter", "Position $position is out of bounds for size ${reviews.size}")
}
}
override fun getItemCount(): Int = reviews.size
inner class ReviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val ivProduct: ImageView = itemView.findViewById(R.id.iv_product)
private val tvProductName: TextView = itemView.findViewById(R.id.tv_product_name)
private val tvReviewScore: TextView = itemView.findViewById(R.id.tv_review_score)
private val tvReviewDate: TextView = itemView.findViewById(R.id.tv_review_date)
private val tvUsername: TextView = itemView.findViewById(R.id.tv_username)
private val tvReviewDesc: TextView = itemView.findViewById(R.id.tv_review_desc)
private val ivMenu: ImageView = itemView.findViewById(R.id.iv_menu)
fun bind(review: ReviewsItem) {
val actualScore =
if (fragmentScore == "all") review.rating.toString() else fragmentScore
CoroutineScope(Dispatchers.Main).launch {
val imageUrl = viewModel.getProductImage(review.productId ?: -1)
Glide.with(itemView.context)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.into(ivProduct)
}
tvProductName.text = review.productName
tvReviewScore.text = actualScore
tvReviewDate.text = review.reviewDate
tvUsername.text = review.username
tvReviewDesc.text = review.reviewText
}
}
}

View File

@ -1,32 +1,56 @@
package com.alya.ecommerce_serang.ui.profile.mystore.review package com.alya.ecommerce_serang.ui.profile.mystore.review
import androidx.fragment.app.viewModels
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.databinding.FragmentReviewBinding
import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel import com.alya.ecommerce_serang.utils.SessionManager
import com.google.android.material.tabs.TabLayoutMediator
class ReviewFragment : Fragment() { class ReviewFragment : Fragment() {
companion object { private var _binding: FragmentReviewBinding? = null
fun newInstance() = ReviewFragment() private val binding get() = _binding!!
} private lateinit var sessionManager: SessionManager
private val viewModel: ReviewViewModel by viewModels() private lateinit var viewPagerAdapter: ReviewViewPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// TODO: Use the ViewModel
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
return inflater.inflate(R.layout.fragment_review, container, false) _binding = FragmentReviewBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sessionManager = SessionManager(requireContext())
setupViewPager()
}
private fun setupViewPager() {
viewPagerAdapter = ReviewViewPagerAdapter(requireActivity())
binding.viewPagerReview.adapter = viewPagerAdapter
TabLayoutMediator(binding.tabLayoutReview, binding.viewPagerReview) { tab, position ->
tab.text = when (position) {
0 -> "Semua"
1 -> "5 Bintang"
2 -> "4 Bintang"
3 -> "3 Bintang"
4 -> "2 Bintang"
5 -> "1 Bintang"
else -> "Tab $position"
}
}.attach()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
} }
} }

View File

@ -0,0 +1,123 @@
package com.alya.ecommerce_serang.ui.profile.mystore.review
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ReviewRepository
import com.alya.ecommerce_serang.databinding.FragmentReviewListBinding
import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.alya.ecommerce_serang.utils.viewmodel.ReviewViewModel
class ReviewListFragment : Fragment() {
private var _binding: FragmentReviewListBinding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private lateinit var reviewAdapter: ReviewAdapter
private val viewModel: ReviewViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(SessionManager(requireContext()))
ReviewViewModel(ReviewRepository(apiService))
}
}
private var score: String = "all"
companion object {
private const val ARG_SCORE = "score"
fun newInstance(score: String): ReviewListFragment = ReviewListFragment().apply {
arguments = Bundle().apply {
putString(ARG_SCORE, score)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
score = arguments?.getString(ARG_SCORE) ?: "all"
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentReviewListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
reviewAdapter = ReviewAdapter(viewModel)
binding.rvReview.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = reviewAdapter
}
observeReviewList()
fetchReviewByScore(score)
}
private fun fetchReviewByScore(score: String) {
val normalizedScore = when (score) {
"all" -> "all"
else -> {
val scoreValue = score.toDoubleOrNull() ?: 0.0
when {
scoreValue > 4.5 -> "5"
scoreValue > 3.5 -> "4"
scoreValue > 2.5 -> "3"
scoreValue > 1.5 -> "2"
else -> "1"
}
}
}
viewModel.getReview(normalizedScore)
}
private fun observeReviewList() {
viewModel.review.observe(viewLifecycleOwner) { result ->
when (result) {
is ViewState.Success -> {
val data = result.data.orEmpty().sortedByDescending { it.reviewDate }
binding.progressBar.visibility = View.GONE
if (data.isEmpty()) {
binding.tvEmptyState.visibility = View.VISIBLE
binding.rvReview.visibility = View.GONE
} else {
binding.tvEmptyState.visibility = View.GONE
binding.rvReview.visibility = View.VISIBLE
reviewAdapter.submitList(data)
}
}
is ViewState.Loading -> binding.progressBar.visibility = View.VISIBLE
is ViewState.Error -> {
binding.progressBar.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -0,0 +1,24 @@
package com.alya.ecommerce_serang.ui.profile.mystore.review
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class ReviewViewPagerAdapter(
fragmentActivity: FragmentActivity
) : FragmentStateAdapter(fragmentActivity) {
private val reviewScore = listOf(
"all",
"5",
"4",
"3",
"2",
"1"
)
override fun getItemCount(): Int = reviewScore.size
override fun createFragment(position: Int): Fragment {
return ReviewListFragment.newInstance(reviewScore[position])
}
}

View File

@ -17,6 +17,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmen
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.gson.Gson import com.google.gson.Gson
import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -86,7 +87,7 @@ class SellsAdapter(
Log.d("SellsAdapter", "=== ViewHolder.bind() called ===") Log.d("SellsAdapter", "=== ViewHolder.bind() called ===")
Log.d("SellsAdapter", "Binding order: ${order.orderId} with status: ${order.status}") Log.d("SellsAdapter", "Binding order: ${order.orderId} with status: ${order.status}")
val actualStatus = if (fragmentStatus == "all") order.status ?: "" else fragmentStatus val actualStatus = if (fragmentStatus == "all") order.displayStatus ?: "" else fragmentStatus
adjustDisplay(actualStatus, order) adjustDisplay(actualStatus, order)
tvSellsNumber.text = "No. Pesanan: ${order.orderId}" tvSellsNumber.text = "No. Pesanan: ${order.orderId}"
@ -96,7 +97,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull() val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}" tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString()) tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toInt()) }
val fullImageUrl = when (val img = product?.productImage) { val fullImageUrl = when (val img = product?.productImage) {
is String -> { is String -> {
@ -169,7 +170,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull() val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}" tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString()) tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toInt()) }
val fullImageUrl = when (val img = product?.productImage) { val fullImageUrl = when (val img = product?.productImage) {
is String -> { is String -> {
@ -185,7 +186,7 @@ class SellsAdapter(
.into(ivSellsProduct) .into(ivSellsProduct)
tvSellsQty.text = "${order.orderItems?.size} produk" tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsPrice.text = formatPrice(order.totalAmount.toString()) tvSellsPrice.text = order.totalAmount?.let { formatPrice(it.toInt()) }
} }
"paid" -> { "paid" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -308,10 +309,9 @@ class SellsAdapter(
} }
} }
private fun formatPrice(price: String): String { private fun formatPrice(amount: Int): String {
val priceDouble = price.toDoubleOrNull() ?: 0.0 val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
val formattedPrice = String.format(Locale("id", "ID"), "Rp%,.0f", priceDouble) return formatter.format(amount.toLong()).replace(",00", "")
return formattedPrice
} }
} }
} }

View File

@ -6,6 +6,7 @@ import android.util.Log
import android.widget.Toast 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 com.alya.ecommerce_serang.data.api.response.store.sells.Orders import com.alya.ecommerce_serang.data.api.response.store.sells.Orders
import com.alya.ecommerce_serang.data.api.response.store.sells.OrdersItem import com.alya.ecommerce_serang.data.api.response.store.sells.OrdersItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -13,6 +14,7 @@ import com.alya.ecommerce_serang.data.repository.AddressRepository
import com.alya.ecommerce_serang.data.repository.SellsRepository import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailShipmentBinding import com.alya.ecommerce_serang.databinding.ActivityDetailShipmentBinding
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter
import com.alya.ecommerce_serang.ui.profile.mystore.sells.payment.DetailPaymentActivity
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.AddressViewModel import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
@ -51,6 +53,12 @@ class DetailShipmentActivity : AppCompatActivity() {
finish() finish()
} }
productAdapter = SellsProductAdapter()
binding.rvProductItems.apply {
adapter = productAdapter
layoutManager = LinearLayoutManager(this@DetailShipmentActivity)
}
val sellsJson = intent.getStringExtra("sells_data") val sellsJson = intent.getStringExtra("sells_data")
if (sellsJson != null) { if (sellsJson != null) {
try { try {

View File

@ -8,13 +8,46 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.zip.GZIPOutputStream
object FileUtils { object FileUtils {
private const val TAG = "FileUtils" private const val TAG = "FileUtils"
/** /**
* Creates a temporary file from a URI in the app's cache directory * Compress a file to GZIP format to reduce its size to below 1MB
* @param context The context
* @param uri The URI of the file to compress
* @param maxSize The target size limit in bytes (1MB = 1048576 bytes)
* @return The compressed file, or null if compression failed
*/
fun compressFile(context: Context, uri: Uri, maxSize: Long = 1048576L): File? {
try {
// Create a temporary file for compressed content
val originalFile = createTempFileFromUri(context, uri, "compressed")
val compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.gz")
// Compress the original file into the GZIP file
compressToGZIP(originalFile, compressedFile)
// Check if the compressed file is larger than the allowed size
if (compressedFile.length() <= maxSize) {
Log.d(TAG, "Compression successful. Compressed file size: ${compressedFile.length()} bytes.")
return compressedFile
} else {
// If the file is still too large, you can handle it by reducing quality or adjusting compression logic
Log.e(TAG, "Compressed file exceeds the size limit. Size: ${compressedFile.length()} bytes.")
return null
}
} catch (e: Exception) {
Log.e(TAG, "Error during file compression: ${e.message}", e)
return null
}
}
/**
* Creates a temporary file from the URI in the app's cache directory.
*/ */
fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? { fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? {
try { try {
@ -41,6 +74,23 @@ object FileUtils {
} }
} }
/**
* Compress the input file into a GZIP file.
*/
private fun compressToGZIP(inputFile: File?, outputFile: File) {
FileInputStream(inputFile).use { inputStream ->
FileOutputStream(outputFile).use { fileOutputStream ->
GZIPOutputStream(fileOutputStream).use { gzipOutputStream ->
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
gzipOutputStream.write(buffer, 0, bytesRead)
}
}
}
}
}
/** /**
* Gets the file extension from a URI using ContentResolver * Gets the file extension from a URI using ContentResolver
*/ */

View File

@ -7,8 +7,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.City import com.alya.ecommerce_serang.data.api.dto.City
import com.alya.ecommerce_serang.data.api.dto.Province import com.alya.ecommerce_serang.data.api.dto.Province
import com.alya.ecommerce_serang.data.api.dto.StoreAddress import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressesItem
import com.alya.ecommerce_serang.data.repository.AddressRepository import com.alya.ecommerce_serang.data.repository.AddressRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AddressViewModel(private val addressRepository: AddressRepository) : ViewModel() { class AddressViewModel(private val addressRepository: AddressRepository) : ViewModel() {
@ -21,8 +23,8 @@ class AddressViewModel(private val addressRepository: AddressRepository) : ViewM
private val _cities = MutableLiveData<List<City>>() private val _cities = MutableLiveData<List<City>>()
val cities: LiveData<List<City>> = _cities val cities: LiveData<List<City>> = _cities
private val _storeAddress = MutableLiveData<StoreAddress?>() private val _storeAddress = MutableLiveData<AddressesItem?>()
val storeAddress: LiveData<StoreAddress?> = _storeAddress val storeAddress: LiveData<AddressesItem?> get() = _storeAddress
private val _isLoading = MutableLiveData<Boolean>() private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading val isLoading: LiveData<Boolean> = _isLoading
@ -31,99 +33,249 @@ class AddressViewModel(private val addressRepository: AddressRepository) : ViewM
val errorMessage: LiveData<String> = _errorMessage val errorMessage: LiveData<String> = _errorMessage
private val _saveSuccess = MutableLiveData<Boolean>() private val _saveSuccess = MutableLiveData<Boolean>()
val saveSuccess: LiveData<Boolean> = _saveSuccess val saveSuccess: LiveData<Boolean> get() = _saveSuccess
private val _subdistrictState = MutableLiveData<Result<List<SubdistrictsItem>>>()
val subdistrictState: LiveData<Result<List<SubdistrictsItem>>> = _subdistrictState
var selectedSubdistrict: String? = null
val subdistrict = MutableLiveData<String>()
fun fetchProvinces() { fun fetchProvinces() {
Log.d(TAG, "fetchProvinces() called")
_isLoading.value = true
viewModelScope.launch { viewModelScope.launch {
try { try {
Log.d(TAG, "Calling addressRepository.getProvinces()")
val response = addressRepository.getProvinces() val response = addressRepository.getProvinces()
Log.d(TAG, "Received provinces response: ${response.size} provinces") if (response.isSuccessful) {
_provinces.value = response _provinces.value = response.body()?.data ?: emptyList()
_isLoading.value = false } else {
Log.e("EditAddressVM", "Failed to get provinces: ${response.message()}")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching provinces", e) Log.e("EditAddressVM", "Error getting provinces: ${e.message}")
_errorMessage.value = "Failed to load provinces: ${e.message}"
_isLoading.value = false
} }
} }
} }
fun fetchCities(provinceId: String) { fun fetchCities(provinceId: String) {
Log.d(TAG, "fetchCities() called with provinceId: $provinceId")
_isLoading.value = true
viewModelScope.launch { viewModelScope.launch {
try { try {
Log.d(TAG, "Calling addressRepository.getCities()")
val response = addressRepository.getCities(provinceId) val response = addressRepository.getCities(provinceId)
Log.d(TAG, "Received cities response: ${response.size} cities") if (response.isSuccessful) {
_cities.value = response _cities.value = response.body()?.cities ?: emptyList()
_isLoading.value = false } else {
Log.e("EditAddressVM", "Failed to get cities: ${response.message()}")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching cities", e) Log.e("EditAddressVM", "Error getting cities: ${e.message}")
_errorMessage.value = "Failed to load cities: ${e.message}"
_isLoading.value = false
} }
} }
} }
fun getSubdistrict(cityId: String) {
_subdistrictState.value = Result.Loading
viewModelScope.launch {
try {
selectedSubdistrict = cityId
val result = addressRepository.getListSubdistrict(cityId)
result?.let {
_subdistrictState.postValue(Result.Success(it.subdistricts))
Log.d(TAG, "Cities loaded for province $cityId: ${it.subdistricts.size}")
} ?: run {
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
Log.e(TAG, "City result was null for province $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(Result.Error(Exception(e.message ?: "Error loading cities")))
Log.e(TAG, "Error fetching cities for province $cityId", e)
}
}
}
// fun fetchProvinces() {
// Log.d(TAG, "fetchProvinces() called")
// _isLoading.value = true
// viewModelScope.launch {
// try {
// Log.d(TAG, "Calling addressRepository.getProvinces()")
// val response = addressRepository.getProvinces()
// Log.d(TAG, "Received provinces response: ${response.size} provinces")
// _provinces.value = response
// _isLoading.value = false
// } catch (e: Exception) {
// Log.e(TAG, "Error fetching provinces", e)
// _errorMessage.value = "Failed to load provinces: ${e.message}"
// _isLoading.value = false
// }
// }
// }
// fun fetchCities(provinceId: String) {
// Log.d(TAG, "fetchCities() called with provinceId: $provinceId")
// _isLoading.value = true
// viewModelScope.launch {
// try {
// selecte
// Log.d(TAG, "Calling addressRepository.getCities()")
// val response = addressRepository.getCities(provinceId)
// Log.d(TAG, "Received cities response: ${response.size} cities")
// _cities.value = response
// _isLoading.value = false
// } catch (e: Exception) {
// Log.e(TAG, "Error fetching cities", e)
// _errorMessage.value = "Failed to load cities: ${e.message}"
// _isLoading.value = false
// }
// }
// }
// fun fetchStoreAddress() {
// Log.d(TAG, "fetchStoreAddress() called")
// _isLoading.value = true
// viewModelScope.launch {
// try {
// Log.d(TAG, "Calling addressRepository.getStoreAddress()")
// val response = addressRepository.getStoreAddress()
// Log.d(TAG, "Received store address response: $response")
// _storeAddress.value = response
// _isLoading.value = false
// } catch (e: Exception) {
// Log.e(TAG, "Error fetching store address", e)
// _errorMessage.value = "Failed to load store address: ${e.message}"
// _isLoading.value = false
// }
// }
// }
// fun fetchStoreAddress() {
// viewModelScope.launch {
// try {
// val response = addressRepository.getStoreAddress()
// if (response.isSuccessful) {
// val storeAddress = response.body()?.addresses
// ?.firstOrNull { it.isStoreLocation == true }
//
// if (storeAddress != null) {
// _storeAddress.value = storeAddress
// } else {
// Log.d("EditAddressVM", "No store address found")
// }
// } else {
// Log.e("EditAddressVM", "Failed to get addresses: ${response.message()}")
// }
// } catch (e: Exception) {
// Log.e("EditAddressVM", "Error: ${e.message}")
// }
// }
// }
fun fetchStoreAddress() { fun fetchStoreAddress() {
Log.d(TAG, "fetchStoreAddress() called")
_isLoading.value = true
viewModelScope.launch { viewModelScope.launch {
try { try {
Log.d(TAG, "Calling addressRepository.getStoreAddress()")
val response = addressRepository.getStoreAddress() val response = addressRepository.getStoreAddress()
Log.d(TAG, "Received store address response: $response") if (response.isSuccessful) {
_storeAddress.value = response val storeAddress = response.body()?.addresses
_isLoading.value = false ?.firstOrNull { it.isStoreLocation == true }
if (storeAddress != null) {
_storeAddress.value = storeAddress
} else {
Log.d(TAG, "No store address found")
}
} else {
Log.e(TAG, "Failed to get addresses: ${response.message()}")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching store address", e) Log.e(TAG, "Error: ${e.message}")
_errorMessage.value = "Failed to load store address: ${e.message}"
_isLoading.value = false
} }
} }
} }
fun saveStoreAddress( // fun saveStoreAddress(
provinceId: String, // provinceId: String,
provinceName: String, // provinceName: String,
cityId: String, // cityId: String,
cityName: String, // cityName: String,
street: String, // street: String,
subdistrict: String, // subdistrict: String,
detail: String, // detail: String,
postalCode: String, // postalCode: String,
latitude: Double, // latitude: Double,
longitude: Double // longitude: Double
) { // ) {
Log.d(TAG, "saveStoreAddress() called with provinceId: $provinceId, cityId: $cityId") // Log.d(TAG, "saveStoreAddress() called with provinceId: $provinceId, cityId: $cityId")
_isLoading.value = true // _isLoading.value = true
// viewModelScope.launch {
// try {
// Log.d(TAG, "Calling addressRepository.saveStoreAddress()")
// val success = addressRepository.saveStoreAddress(
// provinceId = provinceId,
// provinceName = provinceName,
// cityId = cityId,
// cityName = cityName,
// street = street,
// subdistrict = subdistrict,
// detail = detail,
// postalCode = postalCode,
// latitude = latitude,
// longitude = longitude
// )
// Log.d(TAG, "Save store address result: $success")
// _saveSuccess.value = success
// _isLoading.value = false
// } catch (e: Exception) {
// Log.e(TAG, "Error saving store address", e)
// _errorMessage.value = "Failed to save address: ${e.message}"
// _isLoading.value = false
// }
// }
// }
fun saveStoreAddress(oldAddress: AddressesItem, newAddress: AddressesItem) {
val params = buildUpdateBody(oldAddress, newAddress)
if (params.isEmpty()) {
Log.d(TAG, "No changes detected")
_saveSuccess.value = false
return
}
viewModelScope.launch { viewModelScope.launch {
try { try {
Log.d(TAG, "Calling addressRepository.saveStoreAddress()") val response = addressRepository.updateAddress(oldAddress.id, params)
val success = addressRepository.saveStoreAddress( _saveSuccess.value = response.isSuccessful
provinceId = provinceId,
provinceName = provinceName,
cityId = cityId,
cityName = cityName,
street = street,
subdistrict = subdistrict,
detail = detail,
postalCode = postalCode,
latitude = latitude,
longitude = longitude
)
Log.d(TAG, "Save store address result: $success")
_saveSuccess.value = success
_isLoading.value = false
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error saving store address", e) Log.e(TAG, "Error: ${e.message}")
_errorMessage.value = "Failed to save address: ${e.message}" _saveSuccess.value = false
_isLoading.value = false
} }
} }
} }
private fun buildUpdateBody(oldAddress: AddressesItem, newAddress: AddressesItem): Map<String, Any> {
val params = mutableMapOf<String, Any>()
fun addIfChanged(key: String, oldValue: Any?, newValue: Any?) {
if (newValue != null && newValue != oldValue) {
params[key] = newValue
}
}
addIfChanged("street", oldAddress.street, newAddress.street)
addIfChanged("province_id", oldAddress.provinceId, newAddress.provinceId)
addIfChanged("detail", oldAddress.detail, newAddress.detail)
addIfChanged("subdistrict", oldAddress.subdistrict, newAddress.subdistrict)
addIfChanged("city_id", oldAddress.cityId, newAddress.cityId)
addIfChanged("village_id", oldAddress.villageId, newAddress.villageId)
addIfChanged("postal_code", oldAddress.postalCode, newAddress.postalCode)
addIfChanged("phone", oldAddress.phone, newAddress.phone)
addIfChanged("recipient", oldAddress.recipient, newAddress.recipient)
addIfChanged("latitude", oldAddress.latitude, newAddress.latitude)
addIfChanged("longitude", oldAddress.longitude, newAddress.longitude)
addIfChanged("is_store_location", oldAddress.isStoreLocation, newAddress.isStoreLocation)
return params
}
} }

View File

@ -7,8 +7,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.ResetPassResponse
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.Result import com.alya.ecommerce_serang.data.repository.Result
@ -27,6 +29,10 @@ class LoginViewModel(private val repository: UserRepository, private val context
private val _message = MutableLiveData<String>() private val _message = MutableLiveData<String>()
val message: LiveData<String> = _message val message: LiveData<String> = _message
private val _resetPasswordState = MutableLiveData<Result<ResetPassResponse>?>()
val resetPasswordState: LiveData<Result<ResetPassResponse>?> = _resetPasswordState
private val sessionManager by lazy { SessionManager(context) } private val sessionManager by lazy { SessionManager(context) }
private fun getAuthenticatedApiService(): ApiService { private fun getAuthenticatedApiService(): ApiService {
@ -69,4 +75,19 @@ class LoginViewModel(private val repository: UserRepository, private val context
} }
} }
fun resetPassword(email: String) {
viewModelScope.launch {
_resetPasswordState.value = Result.Loading
val request = ResetPassReq(emailOrPhone = email)
val result = repository.resetPassword(request)
_resetPasswordState.value = result
}
}
fun clearState() {
_resetPasswordState.value = null
}
} }

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
@ -20,6 +21,8 @@ import java.text.NumberFormat
import java.util.Locale import java.util.Locale
class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() { class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
private var TAG = "MyStoreViewModel"
private val _myStoreProfile = MutableLiveData<Store?>() private val _myStoreProfile = MutableLiveData<Store?>()
val myStoreProfile: LiveData<Store?> = _myStoreProfile val myStoreProfile: LiveData<Store?> = _myStoreProfile
@ -38,6 +41,9 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
private val _balanceResult = MutableLiveData<Result<StoreResponse>>() private val _balanceResult = MutableLiveData<Result<StoreResponse>>()
val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
fun loadMyStore(){ fun loadMyStore(){
viewModelScope.launch { viewModelScope.launch {
when (val result = repository.fetchMyStoreProfile()){ when (val result = repository.fetchMyStoreProfile()){
@ -80,30 +86,40 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
if (store == null) { if (store == null) {
_errorMessage.postValue("Data toko tidak tersedia") _errorMessage.postValue("Data toko tidak tersedia")
Log.e(TAG, "Store data is null")
return@launch return@launch
} }
Log.d("UpdateStoreProfileVM", "Calling repository with params:")
Log.d("UpdateStoreProfileVM", "storeName: $storeName")
Log.d("UpdateStoreProfileVM", "description: $description")
Log.d("UpdateStoreProfileVM", "isOnLeave: $isOnLeave")
Log.d("UpdateStoreProfileVM", "storeType: $storeType")
Log.d("UpdateStoreProfileVM", "storeImage: ${storeImage?.headers}")
val response = repository.updateStoreProfile( val response = repository.updateStoreProfile(
storeName = storeName, storeName = storeName,
storeStatus = "active".toRequestBody(),
storeDescription = description, storeDescription = description,
isOnLeave = isOnLeave, isOnLeave = isOnLeave,
cityId = store.cityId.toString().toRequestBody(),
provinceId = store.provinceId.toString().toRequestBody(),
street = store.street.toRequestBody(),
subdistrict = store.subdistrict.toRequestBody(),
detail = store.detail.toRequestBody(),
postalCode = store.postalCode.toRequestBody(),
latitude = store.latitude.toRequestBody(),
longitude = store.longitude.toRequestBody(),
userPhone = store.phone.toRequestBody(),
storeType = storeType, storeType = storeType,
storeimg = storeImage storeimg = storeImage
) )
if (response.isSuccessful) _updateStoreProfileResult.postValue(response.body())
else _errorMessage.postValue("Gagal memperbarui profil") if (response != null) {
if (response.isSuccessful) {
_updateStoreProfileResult.postValue(response.body())
Log.d(TAG, "Update successful: ${response.body()}")
} else {
_errorMessage.postValue("Gagal memperbarui profil")
Log.e(TAG, "Update failed: ${response.errorBody()?.string()}")
}
} else {
_errorMessage.postValue("Terjadi kesalahan jaringan atau server")
Log.e(TAG, "Repository returned null response")
}
} catch (e: Exception) { } catch (e: Exception) {
_errorMessage.postValue(e.message ?: "Unexpected error") _errorMessage.postValue(e.message ?: "Unexpected error")
Log.e(TAG, "Exception updating store profile", e)
} }
} }
} }
@ -116,13 +132,13 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
result.data.orders.size ?: 0 result.data.orders.size ?: 0
} }
is Result.Error -> { is Result.Error -> {
Log.e("SellsViewModel", "Error getting orders count: ${result.exception.message}") Log.e(TAG, "Error getting orders count: ${result.exception.message}")
0 0
} }
is Result.Loading -> 0 is Result.Loading -> 0
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("SellsViewModel", "Exception getting orders count", e) Log.e(TAG, "Exception getting orders count", e)
0 0
} }
} }
@ -134,7 +150,7 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
statuses.forEach { status -> statuses.forEach { status ->
counts[status] = getTotalOrdersByStatus(status) counts[status] = getTotalOrdersByStatus(status)
Log.d("SellsViewModel", "Status: $status, countOrder=${counts[status]}") Log.d(TAG, "Status: $status, countOrder=${counts[status]}")
} }
return counts return counts
@ -158,6 +174,18 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
} }
} }
fun loadMyStoreProducts() {
viewModelScope.launch {
_productList.value = Result.Loading
try {
val result = repository.fetchMyStoreProducts()
_productList.value = Result.Success(result)
} catch (e: Exception) {
_productList.value = Result.Error(e)
}
}
}
private fun String.toRequestBody(): RequestBody = private fun String.toRequestBody(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this) RequestBody.create("text/plain".toMediaTypeOrNull(), this)
} }

View File

@ -30,6 +30,9 @@ class PaymentInfoViewModel(private val repository: PaymentInfoRepository) : View
private val _deletePaymentSuccess = MutableLiveData<Boolean>() private val _deletePaymentSuccess = MutableLiveData<Boolean>()
val deletePaymentSuccess: LiveData<Boolean> = _deletePaymentSuccess val deletePaymentSuccess: LiveData<Boolean> = _deletePaymentSuccess
var selectedBankName: String? = null
val bankName = MutableLiveData<String>()
fun getPaymentInfo() { fun getPaymentInfo() {
_isLoading.value = true _isLoading.value = true
viewModelScope.launch { viewModelScope.launch {

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