Compare commits

92 Commits

Author SHA1 Message Date
7f4d04ac7a sells and address 2025-08-30 06:49:55 +07:00
593231285a shipment confirmation button 2025-08-29 21:37:57 +07:00
d7ffd29032 Merge remote-tracking branch 'origin/master' 2025-08-29 18:26:28 +07:00
7c7941d5b2 fix register store and product 2025-08-29 18:26:12 +07:00
83c5f2acff add delete fcm token 2025-08-29 18:20:51 +07:00
971d489939 fix layout 2025-08-29 17:58:55 +07:00
9b8c92605c fix hardcode detail sells 2025-08-29 16:27:02 +07:00
bc9b16b4af fix document and hardcode in payment adapter 2025-08-29 15:40:13 +07:00
86b5534cb3 fix category display 2025-08-27 20:42:35 +07:00
7fc6458f9b update dialog pop up and fix display picture 2025-08-27 20:24:39 +07:00
66595fcb48 fix logo and add dialog pop up 2025-08-27 01:30:22 +07:00
57f3c463cb update logo margin 2025-08-26 14:41:52 +07:00
96f6e8c251 update logo 2025-08-26 14:33:28 +07:00
3627cdd151 resize logo 2025-08-26 13:57:22 +07:00
442f9fc10c add logo, fix product size, detail product 2025-08-26 13:40:21 +07:00
9273e01324 fix register store and product 2025-08-26 03:26:58 +07:00
cef4bfa2b2 Merge remote-tracking branch 'origin/master' 2025-08-23 09:09:50 +07:00
1eb135d48e change password 2025-08-23 09:09:35 +07:00
fe5ecf28e5 Add files via upload 2025-08-23 00:18:51 +07:00
3d733b7e0f Merge remote-tracking branch 'origin/master' 2025-08-23 00:02:56 +07:00
b3d2527ebc fix register address 2025-08-23 00:02:30 +07:00
16a0a33f11 update bank name and fix 2025-08-22 23:19:59 +07:00
45fddf6116 add splash screen 2025-08-22 19:34:11 +07:00
1d9399fd4d Merge remote-tracking branch 'origin/master' 2025-08-22 19:04:01 +07:00
0887a7e898 change password 2025-08-22 19:03:39 +07:00
29fb55e3c0 fix restore 2025-08-22 14:54:28 +07:00
2f16542e5e fix UI evidence 2025-08-22 14:38:02 +07:00
2f28a23114 Merge remote-tracking branch 'origin/master' 2025-08-22 13:47:20 +07:00
94c081e839 fix UI 2025-08-22 13:46:42 +07:00
d32bdf65fe update checkout 2025-08-22 13:08:06 +07:00
9162b2cc60 update detail store 2025-08-22 12:11:54 +07:00
8eac90311e fix search bar 2025-08-22 11:31:52 +07:00
9cd0675d82 fix progress bar loading 2025-08-22 11:21:07 +07:00
f88a5a46ad Merge remote-tracking branch 'origin/master' 2025-08-22 04:14:04 +07:00
421c20cc4b approval status 2025-08-22 04:13:37 +07:00
792e247eaa fix fcm and update toast 2025-08-22 01:08:25 +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
237 changed files with 11720 additions and 3482 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

View File

@ -4,14 +4,27 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-08T14:50:55.425322500Z"> <DropdownSelection timestamp="2025-08-29T16:47:53.924316Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Gracia Hotmauli\.android\avd\Pixel_8_2_2.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Gracia Hotmauli\.android\avd\Pixel_9_3.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
<DialogSelection /> <DialogSelection>
<targets>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Gracia Hotmauli\.android\avd\Pixel_9_3.avd" />
</handle>
</Target>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Gracia Hotmauli\.android\avd\Pixel_8.avd" />
</handle>
</Target>
</targets>
</DialogSelection>
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

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="DESCENDING" />
</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

@ -1,4 +1,5 @@
import java.util.Properties import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
@ -125,6 +126,10 @@ dependencies {
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")
//Splash screen
implementation("androidx.core:core-splashscreen:1.0.0")
//pdf compression
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
} }

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,21 @@
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.profile.ChangePasswordActivity"
android:exported="false" />
<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 +63,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 +91,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"
@ -159,7 +173,8 @@
<activity <activity
android:name=".ui.auth.RegisterActivity" android:name=".ui.auth.RegisterActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -0,0 +1,6 @@
package com.alya.ecommerce_serang.data.api.dto
data class ChangePasswordRequest(
val currentPassword: String,
val newPassword: String
)

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,21 @@
package com.alya.ecommerce_serang.data.api.dto
import com.google.gson.annotations.SerializedName
import java.io.File
data class PaymentUpdate(
@field:SerializedName("id")
val id: Int? = null,
@field:SerializedName("bank_name")
val bankName: String,
@field:SerializedName("bank_num")
val bankNum: String,
@field:SerializedName("account_name")
val accountName: String,
@field:SerializedName("qris_image")
val qrisImage: File? = 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

@ -10,9 +10,6 @@ data class Store(
@field:SerializedName("store_status") @field:SerializedName("store_status")
val storeStatus: String, val storeStatus: String,
@field:SerializedName("sppirt")
val sppirt: String,
@field:SerializedName("user_name") @field:SerializedName("user_name")
val userName: String, val userName: String,
@ -37,9 +34,6 @@ data class Store(
@field:SerializedName("user_phone") @field:SerializedName("user_phone")
val userPhone: String, val userPhone: String,
@field:SerializedName("halal")
val halal: String,
@field:SerializedName("id") @field:SerializedName("id")
val id: Int, val id: Int,
@ -98,5 +92,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 ChangePassResponse(
@field:SerializedName("message")
val message: String? = null
)

View File

@ -0,0 +1,24 @@
package com.alya.ecommerce_serang.data.api.response.auth
import com.google.gson.annotations.SerializedName
data class DeleteFCMResponse(
@field:SerializedName("message")
val message: String,
@field:SerializedName("user")
val user: UserFCM
)
data class UserFCM(
@field:SerializedName("name")
val name: String,
@field:SerializedName("id")
val id: Int,
@field:SerializedName("email")
val email: 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

@ -62,6 +62,9 @@ data class Product(
@field:SerializedName("wholesale_min_item") @field:SerializedName("wholesale_min_item")
val wholesaleMinItem: Int? = null, val wholesaleMinItem: Int? = null,
@field:SerializedName("status")
val status: String,
@field:SerializedName("min_order") @field:SerializedName("min_order")
val minOrder: Int, val minOrder: Int,
@ -82,7 +85,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

@ -1,18 +1,13 @@
package com.alya.ecommerce_serang.data.api.response.store package com.alya.ecommerce_serang.data.api.response.store
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.response.store.profile.Shipping
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class StoreResponse( data class StoreResponse(
val message: String, val message: String,
val store: Store val store: Store,
) val shipping: List<Shipping> = emptyList(),
val payment: List<Payment> = emptyList()
data class Store(
@SerializedName("store_id") val storeId: Int,
@SerializedName("store_status") val storeStatus: String,
@SerializedName("store_name") val storeName: String,
@SerializedName("user_name") val userName: String,
val email: String,
@SerializedName("user_phone") val userPhone: String,
val balance: 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

@ -1,10 +1,10 @@
package com.alya.ecommerce_serang.data.api.retrofit package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
import com.alya.ecommerce_serang.data.api.dto.ChangePasswordRequest
import com.alya.ecommerce_serang.data.api.dto.CityResponse import com.alya.ecommerce_serang.data.api.dto.CityResponse
import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.ConfirmPaymentRequest import com.alya.ecommerce_serang.data.api.dto.ConfirmPaymentRequest
@ -16,9 +16,9 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.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
@ -26,7 +26,8 @@ import com.alya.ecommerce_serang.data.api.dto.StoreAddressResponse
import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse
import com.alya.ecommerce_serang.data.api.response.auth.DeleteFCMResponse
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.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
@ -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,33 +55,36 @@ 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.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
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@ -90,7 +95,6 @@ import retrofit2.http.PUT
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.PartMap import retrofit2.http.PartMap
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService { interface ApiService {
@POST("registeruser") @POST("registeruser")
@ -103,8 +107,9 @@ interface ApiService {
@Body verifRegisReq: VerifRegisReq @Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse ):VerifRegisterResponse
@GET("checkstore") @PUT("deletefcm")
suspend fun checkStore (): Response<CheckStoreResponse> suspend fun deleteFCMToken (
): DeleteFCMResponse
@Multipart @Multipart
@POST("registerstore") @POST("registerstore")
@ -195,11 +200,6 @@ interface ApiService {
@Path("id") orderId: Int @Path("id") orderId: Int
): Response<OrderDetailResponse> ): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart @Multipart
@POST("order/addevidence") @POST("order/addevidence")
suspend fun addEvidenceMultipart( suspend fun addEvidenceMultipart(
@ -247,15 +247,9 @@ interface ApiService {
@GET("mystore") @GET("mystore")
suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse> suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse>
@GET("mystore")
suspend fun getStoreAddress(): Response<StoreAddressResponse>
@GET("mystore/product") // Replace with actual endpoint @GET("mystore/product") // Replace with actual endpoint
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse> suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@GET("category")
fun getCategories(): Call<CategoryResponse>
@Multipart @Multipart
@POST("store/createproduct") @POST("store/createproduct")
suspend fun addProduct( suspend fun addProduct(
@ -365,9 +359,6 @@ interface ApiService {
@GET("store/topup") @GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse> suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart @Multipart
@POST("store/createtopup") @POST("store/createtopup")
suspend fun addBalanceTopUp( suspend fun addBalanceTopUp(
@ -379,39 +370,37 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody @Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse> ): Response<BalanceTopUpResponse>
@PUT("store/payment/update")
suspend fun paymentConfirmation(
@Body confirmPaymentReq : PaymentConfirmRequest
): Response<PaymentConfirmationResponse>
@Multipart @Multipart
@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>
@Multipart @Multipart
@POST("mystore/payment/add") @PUT("mystore/edit")
suspend fun addPaymentInfo( suspend fun updateStoreApprovalMultipart(
@Part("bank_name") bankName: RequestBody, @Part("store_name") storeName: RequestBody,
@Part("bank_num") bankNum: RequestBody, @Part("store_description") storeDescription: RequestBody,
@Part("account_name") accountName: RequestBody, @Part("store_type_id") storeTypeId: RequestBody,
@Part qris: MultipartBody.Part? @Part("latitude") storeLatitude: RequestBody,
): Response<GenericResponse> @Part("longitude") storeLongitude: RequestBody,
@Part("province_id") storeProvince: RequestBody,
@Part("city_id") storeCity: RequestBody,
@Part("subdistrict") storeSubdistrict: RequestBody,
@Part("village_id") storeVillage: RequestBody,
@Part("street") storeStreet: RequestBody,
@Part("postal_code") storePostalCode: RequestBody,
@Part("detail") storeAddressDetail: RequestBody,
@Part("user_phone") storeUserPhone: RequestBody,
@Part storeimg: MultipartBody.Part?,
@Part ktp: MultipartBody.Part?,
@Part npwp: MultipartBody.Part?,
@Part nib: MultipartBody.Part?
): Response<StoreDataResponse>
@Multipart @Multipart
@POST("mystore/payment/add") @POST("mystore/payment/add")
@ -422,6 +411,16 @@ interface ApiService {
@Part qris: MultipartBody.Part? @Part qris: MultipartBody.Part?
): Response<AddPaymentInfoResponse> ): Response<AddPaymentInfoResponse>
@Multipart
@PUT("mystore/payment/edit")
suspend fun updatePaymentInfo(
@Part("payment_info_id") paymentInfoId: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part qris: MultipartBody.Part? = null
): Response<GenericResponse>
@DELETE("mystore/payment/delete/{id}") @DELETE("mystore/payment/delete/{id}")
suspend fun deletePaymentInfo( suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int @Path("id") paymentMethodId: Int
@ -451,6 +450,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
@ -459,15 +464,6 @@ interface ApiService {
@GET("search") @GET("search")
suspend fun getSearchHistory(): Response<SearchHistoryResponse> suspend fun getSearchHistory(): Response<SearchHistoryResponse>
@Multipart
@POST("sendchat")
suspend fun sendChatLine(
@Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody?,
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@Multipart @Multipart
@POST("store/sendchat") @POST("store/sendchat")
suspend fun sendChatMessageStore( suspend fun sendChatMessageStore(
@ -507,4 +503,33 @@ 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>
@POST("changepass")
suspend fun changePassword(
@Body request: ChangePasswordRequest
): Response<ChangePassResponse>
@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,26 +1,33 @@
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.PaymentUpdate
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
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.store.StoreResponse
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.sells.OrderListResponse import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import java.io.File
import java.io.IOException import java.io.IOException
class MyStoreRepository(private val apiService: ApiService) { class MyStoreRepository(private val apiService: ApiService) {
suspend fun fetchMyStoreProfile(): Result<Store?> { suspend fun fetchMyStoreProfile(): Result<StoreResponse?> {
return try { return try {
val response = apiService.getStore() val response = apiService.getMyStoreData()
if (response.isSuccessful) { if (response.isSuccessful) {
val storeResponse: StoreResponse? = response.body() val storeResponse = response.body()
Result.Success(storeResponse?.store) Result.Success(storeResponse)
} else { } else {
val errorMessage = response.errorBody()?.string() ?: "Unknown API error" val errorMessage = response.errorBody()?.string() ?: "Unknown API error"
Log.e("MyStoreRepository", "Error: $errorMessage") Log.e("MyStoreRepository", "Error: $errorMessage")
@ -52,48 +59,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 +126,209 @@ 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
}
}
suspend fun updateStoreApproval(
storeName: RequestBody,
description: RequestBody,
storeType: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
storeProvince: RequestBody,
storeCity: RequestBody,
storeSubdistrict: RequestBody,
storeVillage: RequestBody,
storeStreet: RequestBody,
storePostalCode: RequestBody,
storeAddressDetail: RequestBody,
userPhone: RequestBody,
paymentsToUpdate: List<PaymentUpdate> = emptyList(),
paymentIdToDelete: List<Int> = emptyList(),
storeCourier: List<String>? = null,
storeImage: MultipartBody.Part?,
ktpImage: MultipartBody.Part?,
npwpDocument: MultipartBody.Part?,
nibDocument: MultipartBody.Part?
): Response<StoreDataResponse>? {
return try {
Log.d(TAG, "Updating store profile & address for approval...")
val profileResp = apiService.updateStoreApprovalMultipart(
storeName = storeName,
storeDescription = description,
storeTypeId = storeType,
storeLatitude = latitude,
storeLongitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
storeUserPhone = userPhone,
storeimg = storeImage,
ktp = ktpImage,
npwp = npwpDocument,
nib = nibDocument
)
if (!profileResp.isSuccessful) {
Log.e(TAG, "Profile update failed: ${profileResp.code()} ${profileResp.errorBody()?.string()}")
return profileResp // short-circuit; let caller inspect the failure
}
// 2) Payments: delete, then upsert (safer if youre changing accounts)
if (paymentIdToDelete.isNotEmpty() || paymentsToUpdate.isNotEmpty()) {
Log.d(TAG, "Synchronizing payments: delete=${paymentIdToDelete.size}, upsert=${paymentsToUpdate.size}")
}
// 2a) Delete payments
paymentIdToDelete.forEach { id ->
runCatching {
apiService.deletePaymentInfo(id)
}.onSuccess {
if (!it.isSuccessful) {
Log.e(TAG, "Delete payment $id failed: ${it.code()} ${it.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted payment $id")
}
}.onFailure { e ->
Log.e(TAG, "Delete payment $id exception", e)
}
}
// 2b) Upsert payments (add if id==null, else update)
paymentsToUpdate.forEach { item ->
runCatching {
// --- CHANGE HERE if your PaymentUpdate field names differ ---
val id = item.id // Int? (null => add)
val bankName = item.bankName // String
val bankNum = item.bankNum // String
val accountName = item.accountName // String
val qrisImage = item.qrisImage // File? (Optional)
// -----------------------------------------------------------
if (id == null) {
// ADD
val resp = apiService.addPaymentInfoDirect(
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
accountName = accountName.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Add payment failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added payment: $bankName/$bankNum")
}
} else {
// UPDATE
val resp = apiService.updatePaymentInfo(
paymentInfoId = id.toString().toPlain(),
accountName = accountName.toPlain(),
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Update payment $id failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Updated payment $id: $bankName/$bankNum")
}
}
}.onFailure { e ->
Log.e(TAG, "Upsert payment exception", e)
}
}
// 3) Shipping: sync to desiredCouriers (if provided)
storeCourier?.let { desired ->
try {
val current = apiService.getStoreData().let { resp ->
if (resp.isSuccessful) {
resp.body()?.shipping?.mapNotNull { it.courier } ?: emptyList()
} else {
Log.e(TAG, "Failed to read current shipping: ${resp.code()} ${resp.errorBody()?.string()}")
emptyList()
}
}
val desiredSet = desired.toSet()
val currentSet = current.toSet()
val toAdd = (desiredSet - currentSet).toList()
val toDel = (currentSet - desiredSet).toList()
if (toAdd.isNotEmpty()) {
val addResp = apiService.addShippingService(ShippingServiceRequest(couriers = toAdd))
if (!addResp.isSuccessful) {
Log.e(TAG, "Add couriers failed: ${addResp.code()} ${addResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added couriers: $toAdd")
}
}
if (toDel.isNotEmpty()) {
val delResp = apiService.deleteShippingService(ShippingServiceRequest(couriers = toDel))
if (!delResp.isSuccessful) {
Log.e(TAG, "Delete couriers failed: ${delResp.code()} ${delResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted couriers: $toDel")
}
}
} catch (e: Exception) {
Log.e(TAG, "Sync shipping exception", e)
}
}
// Return the profile response (already successful here)
profileResp
} catch (e: Exception) {
Log.e(TAG, "Error updating store approval flow", e)
null
}
}
private fun String.toPlain(): RequestBody =
this.toRequestBody("text/plain".toMediaTypeOrNull())
private fun createQrisPartOrNull(file: File?): MultipartBody.Part? =
file?.let {
val mime = when (it.extension.lowercase()) {
"jpg", "jpeg" -> "image/jpeg"
"png" -> "image/png"
else -> "application/octet-stream"
}.toMediaTypeOrNull()
MultipartBody.Part.createFormData(
"qris",
it.name,
it.asRequestBody(mime)
)
}
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

@ -194,7 +194,7 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
Result.Success(response.body()!!) Result.Success(response.body()!!)
} else { } else {
Result.Error(Exception("Failed to create product: ${response.code()}")) Result.Error(Exception("Failed to create product: ${response.code()} message:${response.message()}"))
} }
} catch (e: Exception) { } catch (e: Exception) {
Result.Error(e) Result.Error(e)

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

@ -3,12 +3,16 @@ package com.alya.ecommerce_serang.data.repository
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.ChangePasswordRequest
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.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.ChangePassResponse
import com.alya.ecommerce_serang.data.api.response.auth.DeleteFCMResponse
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.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
@ -18,9 +22,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 +75,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 +104,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 +283,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 +495,57 @@ 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)
}
}
suspend fun changePassword(currentPassword: String, newPassword: String): Result<ChangePassResponse> {
return try {
val request = ChangePasswordRequest(currentPassword, newPassword)
val response = apiService.changePassword(request) // Make the API call
if (response.isSuccessful) {
val changePassResponse = response.body()
if (changePassResponse != null) {
Result.Success(changePassResponse) // Return success with the response message
} else {
Result.Error(Exception("Empty response from server"))
}
} else {
val errorBody = response.errorBody()?.string() ?: "Unknown error"
Log.e(TAG, "Error changing password: $errorBody")
Result.Error(Exception(errorBody))
}
} catch (e: Exception) {
Result.Error(e)
}
}
suspend fun deleteFCMToken(): DeleteFCMResponse{
return apiService.deleteFCMToken()
}
companion object{ companion object{
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
} }

View File

@ -5,12 +5,9 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import com.alya.ecommerce_serang.R
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
@ -18,6 +15,7 @@ import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityLoginBinding import com.alya.ecommerce_serang.databinding.ActivityLoginBinding
import com.alya.ecommerce_serang.ui.MainActivity import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
@ -41,33 +39,14 @@ class LoginActivity : AppCompatActivity() {
binding = ActivityLoginBinding.inflate(layoutInflater) binding = ActivityLoginBinding.inflate(layoutInflater)
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 ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
// onBackPressedDispatcher.addCallback(this) {
// // Handle the back button event
// }
setupListeners() setupListeners()
observeLoginState() observeLoginState()
FirebaseApp.initializeApp(this) FirebaseApp.initializeApp(this)
// Request FCM token at app startup
} }
private fun setupListeners() { private fun setupListeners() {
@ -76,7 +55,7 @@ class LoginActivity : AppCompatActivity() {
val password = binding.etLoginPassword.text.toString() val password = binding.etLoginPassword.text.toString()
if (email.isEmpty() || password.isEmpty()) { if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Mohon masukkan email atau password dengan benar", Toast.LENGTH_SHORT).show()
} else { } else {
loginViewModel.login(email, password) loginViewModel.login(email, password)
} }
@ -86,6 +65,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() {
@ -99,13 +83,24 @@ class LoginActivity : AppCompatActivity() {
retrieveFCMToken() retrieveFCMToken()
// sessionManager.saveUserId(response.userId) // sessionManager.saveUserId(response.userId)
Toast.makeText(this, "Login Successful", Toast.LENGTH_SHORT).show() PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.checkmark__1_,
title = "Berhasil Masuk"
)
Toast.makeText(this, "Berhasil masuk", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
finish() finish()
} }
is com.alya.ecommerce_serang.data.repository.Result.Error -> { is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show() Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.ic_cancel,
title = "Gagal Masuk"
)
Toast.makeText(this, "Gagal masuk", Toast.LENGTH_LONG).show()
} }
is Result.Loading -> { is Result.Loading -> {
// Show loading state // Show loading state

View File

@ -29,7 +29,7 @@ class OtpBottomSheetDialog(
onRegister(updatedUserData) // Send full data to ViewModel onRegister(updatedUserData) // Send full data to ViewModel
dismiss() // Close dialog dismiss() // Close dialog
} else { } else {
Toast.makeText(requireContext(), "Please enter OTP", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Silahkan masukkan kode OTP", Toast.LENGTH_SHORT).show()
} }
} }
return view return view

View File

@ -6,6 +6,7 @@ import android.util.Log
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.splashscreen.SplashScreen.Companion.installSplashScreen
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
@ -39,28 +40,12 @@ class RegisterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Thread.sleep(3000)
installSplashScreen()
binding = ActivityRegisterBinding.inflate(layoutInflater) binding = ActivityRegisterBinding.inflate(layoutInflater)
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,20 +89,72 @@ class RegisterActivity : AppCompatActivity() {
} }
} }
// Function to navigate to the next fragment // In RegisterActivity, add debug to navigateToStep:
fun navigateToStep(step: Int, userData: RegisterRequest?) { fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) { Log.d("RegisterActivity", "=== NAVIGATE TO STEP START ===")
1 -> RegisterStep1Fragment.newInstance() Log.d("RegisterActivity", "Target step: $step")
2 -> RegisterStep2Fragment.newInstance(userData) Log.d("RegisterActivity", "Current fragment count: ${supportFragmentManager.fragments.size}")
3 -> RegisterStep3Fragment.newInstance() Log.d("RegisterActivity", "UserData: ${userData?.email}")
else -> null
Log.d("RegisterActivity", "Navigation called from:")
Thread.currentThread().stackTrace.take(10).forEach { element ->
Log.d("RegisterActivity", " at ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
} }
fragment?.let {
supportFragmentManager.beginTransaction() try {
.replace(R.id.fragment_container, it) val fragment = when (step) {
.addToBackStack(null) 1 -> {
.commit() Log.d("RegisterActivity", "Creating RegisterStep1Fragment")
RegisterStep1Fragment.newInstance()
}
2 -> {
Log.d("RegisterActivity", "Creating RegisterStep2Fragment")
RegisterStep2Fragment.newInstance(userData)
}
3 -> {
Log.d("RegisterActivity", "Creating RegisterStep3Fragment")
RegisterStep3Fragment.newInstance()
}
else -> {
Log.e("RegisterActivity", "Invalid step: $step")
return
}
}
Log.d("RegisterActivity", "Fragment created, starting transaction")
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
Log.d("RegisterActivity", "About to commit transaction")
transaction.commit()
Log.d("RegisterActivity", "Transaction committed")
// Update ViewModel step
registerViewModel.setStep(step)
Log.d("RegisterActivity", "ViewModel step updated to: $step")
} catch (e: Exception) {
Log.e("RegisterActivity", "Exception in navigateToStep: ${e.message}", e)
e.printStackTrace()
}
Log.d("RegisterActivity", "=== NAVIGATE TO STEP END ===")
}
// Handle Android back button - close activity or go to step 1
override fun onBackPressed() {
val currentStep = registerViewModel.currentStep.value ?: 1
if (currentStep == 1) {
// On step 1, exit the activity
super.onBackPressed()
} else {
// On other steps, go back to step 1
navigateToStep(1, null)
} }
} }
} }

View File

@ -1,709 +0,0 @@
package com.alya.ecommerce_serang.ui.auth
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class RegisterStoreActivity : AppCompatActivity() {
private lateinit var binding: ActivityRegisterStoreBinding
private lateinit var sessionManager: SessionManager
private lateinit var provinceAdapter: ProvinceAdapter
private lateinit var cityAdapter: CityAdapter
// Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002
private val PICK_NPWP_REQUEST = 1003
private val PICK_NIB_REQUEST = 1004
private val PICK_PERSETUJUAN_REQUEST = 1005
private val PICK_QRIS_REQUEST = 1006
// Location request code
private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRegisterStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// Apply insets to your root layout
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
windowInsets
}
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
Log.d(TAG, "onCreate: Adapters initialized")
setupDataBinding()
Log.d(TAG, "onCreate: Data binding setup completed")
setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed")
// Setup observers
setupStoreTypesObserver() // Store type observer
setupObservers()
Log.d(TAG, "onCreate: Observers setup completed")
setupMap()
Log.d(TAG, "onCreate: Map setup completed")
setupDocumentUploads()
Log.d(TAG, "onCreate: Document uploads setup completed")
setupCourierSelection()
Log.d(TAG, "onCreate: Courier selection setup completed")
Log.d(TAG, "onCreate: Fetching store types from API")
viewModel.fetchStoreTypes()
Log.d(TAG, "onCreate: Fetching provinces from API")
viewModel.getProvinces()
// Setup register button
binding.btnRegister.setOnClickListener {
Log.d(TAG, "Register button clicked")
if (viewModel.validateForm()) {
Log.d(TAG, "Form validation successful, proceeding with registration")
viewModel.registerStore(this)
} else {
Log.e(TAG, "Form validation failed")
Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "onCreate: RegisterStoreActivity setup completed")
}
private fun setupObservers() {
Log.d(TAG, "setupObservers: Setting up LiveData observers")
// Observe province state
viewModel.provincesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading provinces...")
binding.provinceProgressBar?.visibility = View.VISIBLE
binding.spinnerProvince.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Update adapter with data
provinceAdapter.updateData(state.data)
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
binding.provinceProgressBar?.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
}
}
}
// Observe city state
viewModel.citiesState.observe(this) { state ->
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading cities...")
binding.cityProgressBar?.visibility = View.VISIBLE
binding.spinnerCity.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Update adapter with data
cityAdapter.updateData(state.data)
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
binding.cityProgressBar?.visibility = View.GONE
binding.spinnerCity.isEnabled = true
}
}
}
// Observe registration state
viewModel.registerState.observe(this) { result ->
when (result) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Store registration in progress...")
showLoading(true)
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Store registration successful")
showLoading(false)
Toast.makeText(this, "Toko berhasil didaftarkan", Toast.LENGTH_SHORT).show()
finish() // Return to previous screen
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Store registration failed: ${result.exception.message}")
showLoading(false)
Toast.makeText(this, "Gagal mendaftarkan toko: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
}
}
Log.d(TAG, "setupObservers: Observers setup completed")
}
private fun setupStoreTypesObserver() {
Log.d(TAG, "setupStoreTypesObserver: Setting up store types observer")
// Observe loading state
viewModel.isLoadingType.observe(this) { isLoading ->
if (isLoading) {
Log.d(TAG, "setupStoreTypesObserver: Loading store types...")
// Show loading indicator for store types spinner
binding.spinnerStoreType.isEnabled = false
binding.storeTypeProgressBar?.visibility = View.VISIBLE
} else {
Log.d(TAG, "setupStoreTypesObserver: Store types loading completed")
binding.spinnerStoreType.isEnabled = true
binding.storeTypeProgressBar?.visibility = View.GONE
}
}
// Observe error messages
viewModel.errorMessage.observe(this) { errorMsg ->
if (errorMsg.isNotEmpty()) {
Log.e(TAG, "setupStoreTypesObserver: Error loading store types: $errorMsg")
Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
// Observe store types data
viewModel.storeTypes.observe(this) { storeTypes ->
Log.d(TAG, "setupStoreTypesObserver: Store types loaded: ${storeTypes.size}")
if (storeTypes.isNotEmpty()) {
// Add "Pilih Jenis UMKM" as the first item if it's not already there
val displayList = if (storeTypes.any { it.name == "Pilih Jenis UMKM" || it.id == 0 }) {
Log.d(TAG, "setupStoreTypesObserver: Default item already exists in store types list")
storeTypes
} else {
Log.d(TAG, "setupStoreTypesObserver: Adding default item to store types list")
val defaultItem = StoreTypesItem(name = "Pilih Jenis UMKM", id = 0)
listOf(defaultItem) + storeTypes
}
// Setup spinner with API data
setupStoreTypeSpinner(displayList)
} else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
}
}
Log.d(TAG, "setupStoreTypesObserver: Store types observer setup completed")
}
private fun setupStoreTypeSpinner(storeTypes: List<StoreTypesItem>) {
Log.d(TAG, "setupStoreTypeSpinner: Setting up store type spinner with ${storeTypes.size} items")
// Create a custom adapter to display just the name but hold the whole object
val adapter = object : ArrayAdapter<StoreTypesItem>(
this,
android.R.layout.simple_spinner_item,
storeTypes
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
(view as TextView).text = getItem(position)?.name ?: ""
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getDropDownView(position, convertView, parent)
(view as TextView).text = getItem(position)?.name ?: ""
return view
}
// Override toString to ensure proper display
override fun getItem(position: Int): StoreTypesItem? {
return super.getItem(position)
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
Log.d(TAG, "setupStoreTypeSpinner: Store type adapter created")
// Set adapter to spinner
binding.spinnerStoreType.adapter = adapter
Log.d(TAG, "setupStoreTypeSpinner: Adapter set to spinner")
// Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val selectedItem = adapter.getItem(position)
Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}")
if (selectedItem != null && selectedItem.id > 0) {
// Store the actual ID from the API, not just position
viewModel.storeTypeId.value = selectedItem.id
Log.d(TAG, "Set storeTypeId to ${selectedItem.id}")
} else {
Log.d(TAG, "Default or null store type selected, not setting storeTypeId")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No store type selected")
}
}
// Hide progress bar after setup
binding.storeTypeProgressBar?.visibility = View.GONE
Log.d(TAG, "setupStoreTypeSpinner: Store type spinner setup completed")
}
private fun setupSpinners() {
Log.d(TAG, "setupSpinners: Setting up province and city spinners")
// Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Province selected at position: $position")
val provinceId = provinceAdapter.getProvinceId(position)
if (provinceId != null) {
Log.d(TAG, "Setting province ID: $provinceId")
viewModel.provinceId.value = provinceId
Log.d(TAG, "Fetching cities for province ID: $provinceId")
viewModel.getCities(provinceId)
// Reset city selection when province changes
Log.d(TAG, "Clearing city adapter for new province selection")
cityAdapter.clear()
binding.spinnerCity.setSelection(0)
} else {
Log.e(TAG, "Invalid province ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No province selected")
}
}
// Setup city spinner
binding.spinnerCity.adapter = cityAdapter
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "City selected at position: $position")
val cityId = cityAdapter.getCityId(position)
if (cityId != null) {
Log.d(TAG, "Setting city ID: $cityId")
viewModel.cityId.value = cityId
viewModel.selectedCityId = cityId
} else {
Log.e(TAG, "Invalid city ID for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No city selected")
}
}
// Add initial hints to the spinners
if (provinceAdapter.isEmpty) {
Log.d(TAG, "Adding default province hint")
provinceAdapter.add("Pilih Provinsi")
}
if (cityAdapter.isEmpty) {
Log.d(TAG, "Adding default city hint")
cityAdapter.add("Pilih Kabupaten/Kota")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
}
private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
// Store Image
binding.containerStoreImg.setOnClickListener {
Log.d(TAG, "Store image container clicked, picking image")
pickImage(PICK_STORE_IMAGE_REQUEST)
}
// KTP
binding.containerKtp.setOnClickListener {
Log.d(TAG, "KTP container clicked, picking image")
pickImage(PICK_KTP_REQUEST)
}
// NIB
binding.containerNib.setOnClickListener {
Log.d(TAG, "NIB container clicked, picking document")
pickDocument(PICK_NIB_REQUEST)
}
// NPWP
binding.containerNpwp?.setOnClickListener {
Log.d(TAG, "NPWP container clicked, picking image")
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")
}
private fun pickImage(requestCode: Int) {
Log.d(TAG, "pickImage: Launching image picker with request code: $requestCode")
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
startActivityForResult(intent, requestCode)
}
private fun pickDocument(requestCode: Int) {
Log.d(TAG, "pickDocument: Launching document picker with request code: $requestCode")
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
val mimeTypes = arrayOf("application/pdf", "image/jpeg", "image/png")
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
startActivityForResult(intent, requestCode)
}
private fun setupCourierSelection() {
Log.d(TAG, "setupCourierSelection: Setting up courier checkboxes")
binding.checkboxJne.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNE checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("jne", isChecked)
}
binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNT checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
binding.checkboxPos.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "POS checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("pos", isChecked)
}
Log.d(TAG, "setupCourierSelection: Courier checkboxes setup completed")
}
private fun handleCourierSelection(courier: String, isSelected: Boolean) {
if (isSelected) {
if (!viewModel.selectedCouriers.contains(courier)) {
viewModel.selectedCouriers.add(courier)
Log.d(TAG, "handleCourierSelection: Added courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
}
} else {
viewModel.selectedCouriers.remove(courier)
Log.d(TAG, "handleCourierSelection: Removed courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
}
}
private fun setupMap() {
Log.d(TAG, "setupMap: Setting up map container")
// This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener {
Log.d(TAG, "Map container clicked, checking location permission")
// Request location permission if not granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Location permission not granted, requesting permission")
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST
)
viewModel.latitude.value = "-6.2088"
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()
} else {
Log.d(TAG, "Location permission already granted, setting location")
// Show map selection UI
// This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "setupMap: Map container setup completed")
}
private fun setupDataBinding() {
Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields")
// Two-way data binding for text fields
binding.etStoreName.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.storeName.value = s.toString()
Log.d(TAG, "Store name updated: ${s.toString()}")
}
})
binding.etStoreDescription.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.storeDescription.value = s.toString()
Log.d(TAG, "Store description updated: ${s.toString().take(20)}${if ((s?.length ?: 0) > 20) "..." else ""}")
}
})
binding.etStreet.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.street.value = s.toString()
Log.d(TAG, "Street address updated: ${s.toString()}")
}
})
binding.etPostalCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
try {
viewModel.postalCode.value = s.toString().toInt()
Log.d(TAG, "Postal code updated: ${s.toString()}")
} catch (e: NumberFormatException) {
// Handle invalid input
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
}
}
})
binding.etAddressDetail.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.addressDetail.value = s.toString()
Log.d(TAG, "Address detail updated: ${s.toString()}")
}
})
binding.etBankNumber.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val input = s.toString()
if (input.isNotEmpty()) {
try {
viewModel.bankNumber.value = input.toInt()
Log.d(TAG, "Bank number updated: $input")
} catch (e: NumberFormatException) {
// Handle invalid input if needed
Log.e(TAG, "Failed to parse bank number. Input: $input, Error: $e")
}
} else {
// Handle empty input - perhaps set to 0 or null depending on your requirements
viewModel.bankNumber.value = 0 // or 0
Log.d(TAG, "Bank number set to default: 0")
}
}
})
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()}")
}
})
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")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
val uri = data.data
Log.d(TAG, "onActivityResult: URI received: $uri")
when (requestCode) {
PICK_STORE_IMAGE_REQUEST -> {
Log.d(TAG, "Store image selected")
viewModel.storeImageUri = uri
updateImagePreview(uri, binding.imgStore, binding.layoutUploadStoreImg)
}
PICK_KTP_REQUEST -> {
Log.d(TAG, "KTP image selected")
viewModel.ktpUri = uri
updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp)
}
PICK_NPWP_REQUEST -> {
Log.d(TAG, "NPWP document selected")
viewModel.npwpUri = uri
updateDocumentPreview(binding.layoutUploadNpwp)
}
PICK_NIB_REQUEST -> {
Log.d(TAG, "NIB document selected")
viewModel.nibUri = uri
updateDocumentPreview(binding.layoutUploadNib)
}
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 -> {
Log.w(TAG, "Unknown request code: $requestCode")
}
}
} else {
Log.w(TAG, "File selection canceled or failed")
}
}
private fun updateImagePreview(uri: Uri?, imageView: ImageView, uploadLayout: LinearLayout) {
uri?.let {
Log.d(TAG, "updateImagePreview: Setting image URI: $uri")
imageView.setImageURI(it)
imageView.visibility = View.VISIBLE
uploadLayout.visibility = View.GONE
}
}
private fun updateDocumentPreview(uploadLayout: LinearLayout) {
Log.d(TAG, "updateDocumentPreview: Updating document preview UI")
// For documents, we just show a success indicator
val checkIcon = ImageView(this)
checkIcon.setImageResource(android.R.drawable.ic_menu_gallery)
val successText = TextView(this)
successText.text = "Dokumen berhasil diunggah"
uploadLayout.removeAllViews()
uploadLayout.addView(checkIcon)
uploadLayout.addView(successText)
Log.d(TAG, "updateDocumentPreview: Document preview updated with success indicator")
}
//later implement get location form gps
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, proceed with location selection
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
}
}
}
private fun showLoading(isLoading: Boolean) {
if (isLoading) {
// Show loading indicator
binding.btnRegister.isEnabled = false
binding.btnRegister.text = "Mendaftar..."
} else {
// Hide loading indicator
binding.btnRegister.isEnabled = true
binding.btnRegister.text = "Daftar"
}
}
companion object {
private const val TAG = "RegisterStoreActivity"
}
}

View File

@ -0,0 +1,121 @@
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.AppCompatActivity
import com.alya.ecommerce_serang.R
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.PopUpDialog
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()
observeResetPassword()
}
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()
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.checkmark__1_,
title = "Berhasil Ubah Password",
positiveText = "OK"
)
}
private fun handleError(errorMessage: String) {
Log.e(TAG, "Error: $errorMessage")
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.ic_cancel,
title = "Gagal Ubah Password",
message = errorMessage,
positiveText = "OK"
)
}
}

View File

@ -155,19 +155,20 @@ class RegisterStep1Fragment : Fragment() {
"email" -> { "email" -> {
isEmailValid = isValid isEmailValid = isValid
if (!isValid) { if (!isValid) {
Toast.makeText(requireContext(), "Email is already registered", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Email sudah digunakan. Gunakan email lainnya.", Toast.LENGTH_SHORT).show()
} }
} }
"phone" -> { "phone" -> {
isPhoneValid = isValid isPhoneValid = isValid
if (!isValid) { if (!isValid) {
Toast.makeText(requireContext(), "Phone number is already registered", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Nomor handphone sudah digunakan. Gunakan nomor lainnya. ", Toast.LENGTH_SHORT).show()
} }
} }
} }
} }
is com.alya.ecommerce_serang.data.repository.Result.Error -> { is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Toast.makeText(requireContext(), "Validation failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Gagal melakukan validasi", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Validation failed: ${result.exception.message}")
} }
} }
} }
@ -200,10 +201,18 @@ class RegisterStep1Fragment : Fragment() {
is Result.Error -> { is Result.Error -> {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.btnNext.isEnabled = true binding.btnNext.isEnabled = true
Toast.makeText(requireContext(), "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show() Log.e(TAG, "OTP Request Failed: ${result.exception.message}")
Toast.makeText(requireContext(), "Gagal mendapatkan OTP. Kirim ulang OTP", Toast.LENGTH_SHORT).show()
} }
} }
} }
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() {
@ -222,13 +231,13 @@ class RegisterStep1Fragment : Fragment() {
// Check if all fields are filled // Check if all fields are filled
if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() || if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() ||
username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) { username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) {
Toast.makeText(requireContext(), "Please fill all required fields", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Silahkan lengkapi seluruh isian", Toast.LENGTH_SHORT).show()
return return
} }
// Check if passwords match // Check if passwords match
if (password != confirmPassword) { if (password != confirmPassword) {
Toast.makeText(requireContext(), "Passwords do not match", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Konfirmasi kata sandi tidak sesua. Periksa kembali", Toast.LENGTH_SHORT).show()
return return
} }
@ -246,7 +255,7 @@ class RegisterStep1Fragment : Fragment() {
if (isEmailValid && isPhoneValid) { if (isEmailValid && isPhoneValid) {
requestOtp(email) requestOtp(email)
} else { } else {
Toast.makeText(requireContext(), "Please fix validation errors before proceeding", Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), "Silahkan perbaiki data yang dimasukkan", Toast.LENGTH_SHORT).show()
} }
} }

View File

@ -1,5 +1,7 @@
package com.alya.ecommerce_serang.ui.auth.fragments package com.alya.ecommerce_serang.ui.auth.fragments
import android.content.Context
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
@ -13,6 +15,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -24,11 +27,15 @@ 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.firebase.messaging.FirebaseMessaging
class RegisterStep2Fragment : Fragment() { class RegisterStep2Fragment : Fragment() {
private var _binding: FragmentRegisterStep2Binding? = null private var _binding: FragmentRegisterStep2Binding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var countDownTimer: CountDownTimer? = null
private var timeRemaining = 30
private var isTimerRunning = false
// In RegisterStep2Fragment AND RegisterStep3Fragment: // In RegisterStep2Fragment AND RegisterStep3Fragment:
private val registerViewModel: RegisterViewModel by activityViewModels { private val registerViewModel: RegisterViewModel by activityViewModels {
@ -39,8 +46,8 @@ class RegisterStep2Fragment : Fragment() {
RegisterViewModel(userRepository, orderRepository, requireContext()) RegisterViewModel(userRepository, orderRepository, requireContext())
} }
} }
private var countDownTimer: CountDownTimer? = null // private var countDownTimer: CountDownTimer? = null
private var timeRemaining = 30 // 30 seconds cooldown for resend // private var timeRemaining = 30 // 30 seconds cooldown for resend
companion object { companion object {
@ -88,7 +95,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
@ -112,6 +119,20 @@ class RegisterStep2Fragment : Fragment() {
observeRegistrationState() observeRegistrationState()
observeLoginState() observeLoginState()
Log.d(TAG, "Registration and login state observers set up") Log.d(TAG, "Registration and login state observers set up")
binding.btnBack.setOnClickListener {
Log.d(TAG, "Back button clicked - cleaning up timer and going to step 1")
// Stop the timer before navigating
stopTimer()
// Small delay to ensure timer is properly canceled
binding.root.postDelayed({
// (activity as? RegisterActivity)?.navigateToStep(1, null)
val intent = Intent(requireContext(), RegisterActivity::class.java)
startActivity(intent)
requireActivity().finish()
}, 100)
}
} }
private fun verifyOtp(userData: RegisterRequest?) { private fun verifyOtp(userData: RegisterRequest?) {
@ -119,7 +140,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
} }
@ -129,11 +150,6 @@ class RegisterStep2Fragment : Fragment() {
Log.d(TAG, "Updating user data with OTP: $otp") Log.d(TAG, "Updating user data with OTP: $otp")
registerViewModel.updateUserData(updatedUserData) registerViewModel.updateUserData(updatedUserData)
// For demo purposes, we're just proceeding to Step 3
// In a real app, you would verify the OTP with the server first
// registerViewModel.setStep(3)
// (activity as? RegisterActivity)?.navigateToStep(3, updatedUserData)
registerViewModel.registerUser(updatedUserData) registerViewModel.registerUser(updatedUserData)
} ?: Log.e(TAG, "userData is null, cannot proceed with verification") } ?: Log.e(TAG, "userData is null, cannot proceed with verification")
} }
@ -153,13 +169,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")
@ -170,37 +186,9 @@ class RegisterStep2Fragment : Fragment() {
} ?: Log.e(TAG, "Cannot resend OTP: email is null") } ?: Log.e(TAG, "Cannot resend OTP: email is null")
} }
private fun startResendCooldown() {
Log.d(TAG, "startResendCooldown called")
timeRemaining = 30
binding.tvResendOtp.isEnabled = false
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.soft_gray))
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) {
timeRemaining = (millisUntilFinished / 1000).toInt()
binding.tvTimer.text = "Resend available in 00:${String.format("%02d", timeRemaining)}"
if (timeRemaining % 5 == 0) {
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
}
}
override fun onFinish() {
Log.d(TAG, "Cooldown finished, enabling resend button")
binding.tvTimer.text = "You can now resend the code"
binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0
}
}.start()
}
private fun observeRegistrationState() { private fun observeRegistrationState() {
registerViewModel.message.observe(viewLifecycleOwner) { message -> registerViewModel.message.observe(viewLifecycleOwner) { message ->
Log.d(TAG, "Message from server: $message") Log.d(TAG, "Message from server: $message")
// You can use the message here if needed, e.g., for showing in a specific UI element
// or for storing for later use
} }
registerViewModel.registerState.observe(viewLifecycleOwner) { result -> registerViewModel.registerState.observe(viewLifecycleOwner) { result ->
when (result) { when (result) {
@ -222,7 +210,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")
@ -249,17 +238,14 @@ class RegisterStep2Fragment : Fragment() {
// Save the token in fragment // Save the token in fragment
val accessToken = result.data.accessToken val accessToken = result.data.accessToken
sessionManager.saveToken(accessToken) sessionManager.saveToken(accessToken)
retrieveFCMToken()
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 +255,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)
@ -283,9 +269,116 @@ class RegisterStep2Fragment : Fragment() {
} }
} }
override fun onDestroyView() { private fun retrieveFCMToken() {
super.onDestroyView() FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = requireContext().getSharedPreferences("FCM_PREFS", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("FCM_TOKEN", token).apply()
// Send to your server
sendTokenToServer(token)
}
}
private fun sendTokenToServer(token: String) {
Log.d(TAG, "Would send token to server: $token")
val tokenFcm=FcmReq(
fcmToken = token
)
registerViewModel.sendFcm(tokenFcm)
Log.d(TAG, "Sent token fcm: $token")
}
private fun startResendCooldown() {
Log.d(TAG, "startResendCooldown called")
// Cancel any existing timer first
stopTimer()
timeRemaining = 30
isTimerRunning = true
binding.tvResendOtp.isEnabled = false
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.soft_gray))
countDownTimer = object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if (!isTimerRunning) {
cancel()
return
}
timeRemaining = (millisUntilFinished / 1000).toInt()
// Check if fragment is still attached before updating UI
if (isAdded && _binding != null) {
binding.tvTimer.text = "Kirim ulang OTP dalam waktu 00:${String.format("%02d", timeRemaining)}"
if (timeRemaining % 5 == 0) {
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
}
}
}
override fun onFinish() {
if (!isTimerRunning) return
Log.d(TAG, "Cooldown finished, enabling resend button")
// Check if fragment is still attached before updating UI
if (isAdded && _binding != null) {
binding.tvTimer.text = "Dapat mengirim ulang kode OTP"
binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0
}
isTimerRunning = false
}
}.start()
}
private fun stopTimer() {
Log.d(TAG, "stopTimer called")
isTimerRunning = false
countDownTimer?.cancel() countDownTimer?.cancel()
countDownTimer = null
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause - stopping timer")
stopTimer()
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop - stopping timer")
stopTimer()
}
override fun onDestroyView() {
Log.d(TAG, "onDestroyView - cleaning up")
super.onDestroyView()
// Ensure timer is stopped
stopTimer()
_binding = null _binding = null
} }
override fun onDetach() {
super.onDetach()
Log.d(TAG, "onDetach - final cleanup")
stopTimer()
}
} }

View File

@ -23,11 +23,15 @@ 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.PopUpDialog
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 +53,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"
@ -91,30 +97,34 @@ class RegisterStep3Fragment : Fragment() {
Log.d(TAG, "Auto-filled name: ${it.name}, phone: ${it.phone}") Log.d(TAG, "Auto-filled name: ${it.name}, phone: ${it.phone}")
} }
// Set up province and city dropdowns
setupAutoComplete() setupAutoComplete()
setupEdgeToEdge() setupEdgeToEdge()
// Set up button listeners // Set up button listeners
binding.btnPrevious.setOnClickListener { binding.btnPrevious.setOnClickListener {
// Go back to the previous step val step2Fragment = RegisterStep2Fragment()
parentFragmentManager.popBackStack() parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, step2Fragment)
.commit()
} }
binding.btnRegister.setOnClickListener { binding.btnRegister.setOnClickListener {
submitAddress() PopUpDialog.showConfirmDialog(
context = requireContext(),
title = "Apakah anda yakin data anda sudah benar?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
submitAddress()
}
)
} }
// If user skips address entry
// binding.btnSkip.setOnClickListener {
// showRegistrationSuccess()
// }
// 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 +181,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 +199,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 +235,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 +322,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 +380,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 +398,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 +429,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 +473,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
} }
@ -391,7 +508,9 @@ class RegisterStep3Fragment : Fragment() {
private fun showRegistrationSuccess() { private fun showRegistrationSuccess() {
// Now we can show the success message for the overall registration process // Now we can show the success message for the overall registration process
Toast.makeText(requireContext(), "Registration completed successfully!", Toast.LENGTH_LONG).show()
Toast.makeText(requireContext(), "Berhasil mendaftarkan akun", Toast.LENGTH_LONG).show()
sessionManager.clearAll()
// Navigate to login screen // Navigate to login screen
startActivity(Intent(requireContext(), LoginActivity::class.java)) startActivity(Intent(requireContext(), LoginActivity::class.java))
@ -409,8 +528,5 @@ 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)
@ -38,11 +41,14 @@ class CartActivity : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
binding = ActivityCartBinding.inflate(layoutInflater) binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
sessionManager = SessionManager(this) if (!sessionManager.isLoggedIn()){
apiService = ApiConfig.getApiService(sessionManager) binding.emptyCart.text = "Silahkan masuk terlebih dahulu"
}
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -115,7 +121,7 @@ class CartActivity : AppCompatActivity() {
// Start checkout with the prepared items // Start checkout with the prepared items
startCheckoutWithWholesaleInfo(selectedItems) startCheckoutWithWholesaleInfo(selectedItems)
} else { } else {
Toast.makeText(this, "Please select items from a single store only", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Pilih produk yang sama dengan toko", Toast.LENGTH_SHORT).show()
} }
} }
} else { } else {
@ -134,18 +140,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,12 +193,14 @@ 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 ->
errorMessage?.let { errorMessage?.let {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show() binding.emptyCart.visibility = View.VISIBLE
Log.e("CartActivity", "Error message: $it")
} }
} }
@ -211,6 +253,10 @@ class CartActivity : AppCompatActivity() {
storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap) storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap)
} }
} }
viewModel.productImages.observe(this) { productImages ->
storeAdapter.updateProductImages(productImages)
}
} }
private fun showEmptyState(isEmpty: Boolean) { private fun showEmptyState(isEmpty: Boolean) {
@ -230,6 +276,4 @@ class CartActivity : AppCompatActivity() {
return format.format(amount).replace("Rp", "Rp ") return format.format(amount).replace("Rp", "Rp ")
} }
}
}

View File

@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UpdateCart import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
import com.alya.ecommerce_serang.data.api.response.customer.product.CartItemCheckoutInfo import com.alya.ecommerce_serang.data.api.response.customer.product.CartItemCheckoutInfo
import com.alya.ecommerce_serang.data.api.response.customer.product.Product
import com.alya.ecommerce_serang.data.repository.OrderRepository import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -52,6 +53,12 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
private val _hasConsistentWholesaleStatus = MutableLiveData<Boolean>(true) private val _hasConsistentWholesaleStatus = MutableLiveData<Boolean>(true)
val hasConsistentWholesaleStatus: LiveData<Boolean> = _hasConsistentWholesaleStatus val hasConsistentWholesaleStatus: LiveData<Boolean> = _hasConsistentWholesaleStatus
private val _productDetail = MutableLiveData<Product?>()
val productDetail: LiveData<Product?> get() = _productDetail
private val _productImages = MutableLiveData<Map<Int, String>>()
val productImages: LiveData<Map<Int, String>> = _productImages
fun getCart() { fun getCart() {
_isLoading.value = true _isLoading.value = true
_errorMessage.value = null _errorMessage.value = null
@ -62,6 +69,12 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
_cartItems.value = result.data _cartItems.value = result.data
_isLoading.value = false _isLoading.value = false
result.data.forEach { store ->
store.cartItems.forEach { item ->
loadProductImage(item.productId)
}
}
// After loading cart items, check wholesale status // After loading cart items, check wholesale status
checkWholesaleStatus() checkWholesaleStatus()
} }
@ -404,4 +417,29 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
_hasConsistentWholesaleStatus.value = allSameStatus _hasConsistentWholesaleStatus.value = allSameStatus
} }
fun loadProductImage(productId: Int) {
viewModelScope.launch {
try {
val result = repository.fetchProductDetail(productId)
val imageUrl = result?.product?.image ?: ""
val currentMap = _productImages.value?.toMutableMap() ?: mutableMapOf()
currentMap[productId] = imageUrl
_productImages.value = currentMap
} catch (e: Exception) {
Log.e("CartViewModel", "Error loading product image: ${e.message}")
}
}
}
// fun loadProductDetail(productId: Int) {
// viewModelScope.launch {
// val result = repository.fetchProductDetail(productId)
// val currentMap = _productImages.value?.toMutableMap() ?: mutableMapOf()
// currentMap[productId] = result?.product?.image ?: ""
// _productImages.value = currentMap
// }
// }
} }

View File

@ -11,6 +11,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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.cart.CartItemsItem import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
@ -30,6 +31,12 @@ class StoreAdapter(
private var activeStoreId: Int? = null private var activeStoreId: Int? = null
private var wholesaleStatusMap: Map<Int, Boolean> = mapOf() private var wholesaleStatusMap: Map<Int, Boolean> = mapOf()
private var wholesalePriceMap: Map<Int, Double> = mapOf() private var wholesalePriceMap: Map<Int, Double> = mapOf()
private var productImages: Map<Int, String> = emptyMap()
fun updateProductImages(newImages: Map<Int, String>) {
productImages = newImages
notifyDataSetChanged()
}
companion object { companion object {
private const val VIEW_TYPE_STORE = 0 private const val VIEW_TYPE_STORE = 0
@ -135,7 +142,8 @@ class StoreAdapter(
wholesalePrice, wholesalePrice,
{ isChecked -> onItemCheckChanged(cartItem.cartItemId, store.storeId, isChecked) }, { isChecked -> onItemCheckChanged(cartItem.cartItemId, store.storeId, isChecked) },
{ quantity -> onItemQuantityChanged(cartItem.cartItemId, quantity) }, { quantity -> onItemQuantityChanged(cartItem.cartItemId, quantity) },
{ onItemDeleted(cartItem.cartItemId) } { onItemDeleted(cartItem.cartItemId) },
productImages
) )
} }
} }
@ -197,7 +205,8 @@ class StoreAdapter(
wholesalePrice: Double?, wholesalePrice: Double?,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit,
onQuantityChanged: (Int) -> Unit, onQuantityChanged: (Int) -> Unit,
onDelete: () -> Unit onDelete: () -> Unit,
productImages: Map<Int, String>
) { ) {
// Set product name // Set product name
tvProductName.text = cartItem.productName tvProductName.text = cartItem.productName
@ -216,20 +225,6 @@ class StoreAdapter(
// Set quantity // Set quantity
tvQuantity.text = cartItem.quantity.toString() tvQuantity.text = cartItem.quantity.toString()
// Visual indication for wholesale items
// if (isWholesale) {
// // You can add a background or border to indicate wholesale items
// // For example:
//// itemView.setBackgroundResource(R.drawable.bg_wholesale_item)
// // If you don't have this drawable, you can use a simple color tint instead:
// itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.wholesale_item_bg))
// } else {
// // Reset to default background
//// itemView.setBackgroundResource(R.drawable.bg_normal_item)
// // Or if you don't have this drawable:
// itemView.setBackgroundColor(ContextCompat.getColor(itemView.context, R.color.normal_item_bg))
// }
// Set checkbox state without triggering listener // Set checkbox state without triggering listener
cbItem.setOnCheckedChangeListener(null) cbItem.setOnCheckedChangeListener(null)
cbItem.isChecked = isSelected cbItem.isChecked = isSelected
@ -247,11 +242,16 @@ class StoreAdapter(
onCheckedChange(isChecked) onCheckedChange(isChecked)
} }
// Load product image val fullImageUrl = when (val img = productImages[cartItem.productId]) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> null
}
Glide.with(itemView.context) Glide.with(itemView.context)
.load("https://example.com/images/${cartItem.productId}.jpg") // Assume image URL based on product ID .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct) .into(ivProduct)
// Quantity control // Quantity control

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
@ -123,12 +124,13 @@ class ChatActivity : AppCompatActivity() {
if (token.isEmpty()) { if (token.isEmpty()) {
// User not logged in, redirect to login // User not logged in, redirect to login
Toast.makeText(this, "Please login first", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Silahkan masuk terlebih dahulu", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, LoginActivity::class.java)) startActivity(Intent(this, LoginActivity::class.java))
finish() finish()
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) {
@ -500,7 +506,7 @@ class ChatActivity : AppCompatActivity() {
} }
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(this, "Cannot open product details", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gagal memuat produk", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Error navigating to product detail", e) Log.e(TAG, "Error navigating to product detail", e)
} }
} }
@ -616,7 +622,7 @@ class ChatActivity : AppCompatActivity() {
if (outputFile.exists() && outputFile.length() > 0) { if (outputFile.exists() && outputFile.length() > 0) {
if (outputFile.length() > 5 * 1024 * 1024) { if (outputFile.length() > 5 * 1024 * 1024) {
Log.e(TAG, "File too large: ${outputFile.length()} bytes") Log.e(TAG, "File too large: ${outputFile.length()} bytes")
Toast.makeText(this, "Image too large (max 5MB)", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gambar terlalu besar. Maksimal 1MB", Toast.LENGTH_SHORT).show()
return return
} }

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,10 +1,10 @@
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
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -55,31 +55,46 @@ 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 -> {
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show() // binding.tvEmptyChat.visibility = View.VISIBLE
binding.progressBarChat.visibility = View.VISIBLE
// Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Failed to load chats")
} }
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 +104,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

@ -6,6 +6,7 @@ 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
import android.widget.Toast
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -32,6 +33,7 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.setLightStatusBar import com.alya.ecommerce_serang.utils.setLightStatusBar
import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState import com.alya.ecommerce_serang.utils.viewmodel.HomeUiState
import com.alya.ecommerce_serang.utils.viewmodel.HomeViewModel import com.alya.ecommerce_serang.utils.viewmodel.HomeViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
//@AndroidEntryPoint //@AndroidEntryPoint
@ -67,12 +69,10 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initUi() initUi()
setupRecyclerView() setupRecyclerView()
observeData() observeData()
setupSearchView() setupSearchView()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -140,24 +140,26 @@ class HomeFragment : Fragment() {
viewModel.uiState.collect { state -> viewModel.uiState.collect { state ->
when (state) { when (state) {
is HomeUiState.Loading -> { is HomeUiState.Loading -> {
binding.loading.root.isVisible = true binding.loadingAll.root.visibility = View.VISIBLE
binding.error.root.isVisible = false binding.error.root.isVisible = false
binding.home.isVisible = false binding.home.isVisible = false
} }
is HomeUiState.Success -> { is HomeUiState.Success -> {
binding.loading.root.isVisible = false val products = state.products
viewModel.loadStoresForProducts(products)
delay(2000)
binding.loadingAll.root.visibility = View.GONE
binding.error.root.isVisible = false binding.error.root.isVisible = false
binding.home.isVisible = true binding.home.isVisible = true
val products = state.products
viewModel.loadStoresForProducts(products) // << add this here
productAdapter?.updateLimitedProducts(products) productAdapter?.updateLimitedProducts(products)
} }
is HomeUiState.Error -> { is HomeUiState.Error -> {
binding.loading.root.isVisible = false binding.loadingAll.root.visibility = View.GONE
binding.error.root.isVisible = true binding.error.root.isVisible = true
binding.home.isVisible = false binding.home.isVisible = false
binding.error.errorMessage.text = state.message // binding.error.errorMessage.text = state.message
Log.e("HomeFragment", "Error load data: ${state.message}")
Toast.makeText(requireContext(), "Terjadi kendala. Muat ulang halaman", Toast.LENGTH_SHORT) .show()
binding.error.retryButton.setOnClickListener { binding.error.retryButton.setOnClickListener {
viewModel.retry() viewModel.retry()
} }
@ -166,7 +168,6 @@ class HomeFragment : Fragment() {
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
@ -208,25 +209,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 +230,4 @@ class HomeFragment : Fragment() {
categoryAdapter = null categoryAdapter = null
_binding = null _binding = null
} }
// private fun showLoading(isLoading: Boolean) {
// binding.progressBar.isVisible = isLoading
// }
} }

View File

@ -7,12 +7,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.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
@ -106,6 +109,11 @@ class SearchHomeFragment : Fragment() {
} }
}) })
val searchText = findViewById<TextView>(androidx.appcompat.R.id.search_src_text)
searchText.textSize = 14f // in sp
searchText.setHintTextColor(ContextCompat.getColor(context, R.color.black_200))
searchText.setTextColor(ContextCompat.getColor(context, R.color.black))
if (args.query.isNullOrEmpty()) { if (args.query.isNullOrEmpty()) {
requestFocus() requestFocus()
post { post {

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

@ -78,4 +78,4 @@ import com.google.firebase.messaging.RemoteMessage
val notificationId = System.currentTimeMillis().toInt() val notificationId = System.currentTimeMillis().toInt()
notificationManager.notify(notificationId, notificationBuilder.build()) notificationManager.notify(notificationId, notificationBuilder.build())
} }
} }

View File

@ -1,9 +1,11 @@
package com.alya.ecommerce_serang.ui.order package com.alya.ecommerce_serang.ui.order
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
@ -13,37 +15,57 @@ import com.bumptech.glide.Glide
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
class CartCheckoutAdapter(private val checkoutData: CheckoutData) : class CartCheckoutAdapter(
RecyclerView.Adapter<CartCheckoutAdapter.SellerViewHolder>() { private val checkoutData: CheckoutData
) : RecyclerView.Adapter<CartCheckoutAdapter.SellerViewHolder>() {
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root) private var productImages: Map<Int, String> = emptyMap()
private val viewHolders = mutableListOf<SellerViewHolder>() // Keep references
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder { class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root) {
val binding = ItemOrderSellerBinding.inflate( val childAdapter = MultiCartItemsAdapter(emptyList(), emptyMap())
LayoutInflater.from(parent.context), parent, false init {
) binding.rvSellerOrderProduct.apply {
return SellerViewHolder(binding) layoutManager = LinearLayoutManager(binding.root.context)
} adapter = childAdapter
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView with multiple items
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = MultiCartItemsAdapter(checkoutData.cartItems)
isNestedScrollingEnabled = false isNestedScrollingEnabled = false
} }
} }
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val holder = SellerViewHolder(binding)
viewHolders.add(holder) // Keep reference
return holder
}
fun updateProductImages(newImages: Map<Int, String>) {
productImages = newImages
// Update all existing child adapters
viewHolders.forEach { holder ->
holder.childAdapter.updateProductImages(newImages)
}
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
holder.binding.tvStoreName.text = checkoutData.sellerName
holder.childAdapter.updateData(checkoutData.cartItems)
holder.childAdapter.updateProductImages(productImages) // Apply current images
}
override fun onViewRecycled(holder: SellerViewHolder) {
super.onViewRecycled(holder)
viewHolders.remove(holder) // Clean up reference
}
} }
class MultiCartItemsAdapter(private val cartItems: List<CartItemsItem>) : class MultiCartItemsAdapter(
RecyclerView.Adapter<MultiCartItemsAdapter.CartItemViewHolder>() { private var cartItems: List<CartItemsItem> = emptyList(),
private var productImages: Map<Int, String> = emptyMap()
) : RecyclerView.Adapter<MultiCartItemsAdapter.CartItemViewHolder>() {
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root) class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
@ -56,24 +78,57 @@ class MultiCartItemsAdapter(private val cartItems: List<CartItemsItem>) :
override fun getItemCount(): Int = cartItems.size override fun getItemCount(): Int = cartItems.size
fun updateProductImages(images: Map<Int, String>) {
Log.d("MultiCartItemsAdapter", "updateProductImages called with: $images")
Log.d("MultiCartItemsAdapter", "Current cartItems productIds: ${cartItems.map { it.productId }}")
productImages = images
notifyDataSetChanged()
Log.d("MultiCartItemsAdapter", "notifyDataSetChanged() called")
}
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) { override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
val item = cartItems[position] val item = cartItems[position]
Log.d("MultiCartItemsAdapter", "onBindViewHolder - position: $position, productId: ${item.productId}")
Log.d("MultiCartItemsAdapter", "Available images: $productImages")
with(holder.binding) { with(holder.binding) {
// Set cart item details
tvProductName.text = item.productName tvProductName.text = item.productName
tvProductQuantity.text = "${item.quantity} buah" tvProductQuantity.text = "${item.quantity} buah"
tvProductPrice.text = formatCurrency(item.price.toDouble()) tvProductPrice.text = formatCurrency(item.price.toDouble())
// Load placeholder image val img = productImages[item.productId]
Log.d("MultiCartItemsAdapter", "Image for productId ${item.productId}: $img")
val fullImageUrl = when (img) {
is String -> {
val url = if (img.startsWith("/")) BASE_URL + img.substring(1) else img
Log.d("MultiCartItemsAdapter", "Full image URL: $url")
url
}
else -> {
Log.d("MultiCartItemsAdapter", "No image found, using placeholder")
null
}
}
Log.d("MultiCartItemsAdapter", "Loading image with Glide: $fullImageUrl")
Glide.with(ivProduct.context) Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image) // Add error handling
.into(ivProduct) .into(ivProduct)
} }
} }
// Minimal helpers to update adapter data from parent adapter
fun updateData(items: List<CartItemsItem>) {
cartItems = items
notifyDataSetChanged()
}
private fun formatCurrency(amount: Double): String { private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID")) val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "") return formatter.format(amount).replace(",00", "")
} }
} }

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
@ -25,6 +26,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.ui.order.address.AddressActivity import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -34,6 +36,8 @@ class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding private lateinit var binding: ActivityCheckoutBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var paymentAdapter: PaymentMethodAdapter? = null private var paymentAdapter: PaymentMethodAdapter? = null
private var cartCheckoutAdapter: CartCheckoutAdapter? = null
private var checkoutSellerAdapter: CheckoutSellerAdapter? = null
private var paymentMethodsLoaded = false private var paymentMethodsLoaded = false
private val viewModel: CheckoutViewModel by viewModels { private val viewModel: CheckoutViewModel by viewModels {
@ -95,26 +99,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() {
@ -136,8 +157,18 @@ class CheckoutActivity : AppCompatActivity() {
// Observe address details // Observe address details
viewModel.addressDetails.observe(this) { address -> viewModel.addressDetails.observe(this) { address ->
binding.tvPlacesAddress.text = address?.recipient if (address != null) {
binding.tvAddress.text = "${address?.street}, ${address?.subdistrict}" // Show selected address
binding.containerEmptyAddress.visibility = View.GONE
binding.containerAddress.visibility = View.VISIBLE
binding.tvPlacesAddress.text = address.recipient
binding.tvAddress.text = "${address.street}, ${address.subdistrict}"
} else {
// Show empty address state
binding.containerEmptyAddress.visibility = View.VISIBLE
binding.containerAddress.visibility = View.GONE
}
} }
viewModel.availablePaymentMethods.observe(this) { paymentMethods -> viewModel.availablePaymentMethods.observe(this) { paymentMethods ->
@ -154,9 +185,7 @@ class CheckoutActivity : AppCompatActivity() {
// Update the adapter ONLY if it exists // Update the adapter ONLY if it exists
paymentAdapter?.let { adapter -> paymentAdapter?.let { adapter ->
// This line was causing issues - using setSelectedPayment instead of setSelectedPaymentName
adapter.setSelectedPaymentId(selectedPayment.id) adapter.setSelectedPaymentId(selectedPayment.id)
Log.d("CheckoutActivity", "Updated adapter with selected payment: ${selectedPayment.id}") Log.d("CheckoutActivity", "Updated adapter with selected payment: ${selectedPayment.id}")
} }
} }
@ -165,33 +194,47 @@ 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
viewModel.errorMessage.observe(this) { message -> viewModel.errorMessage.observe(this) { message ->
if (message.isNotEmpty()) { if (message.isNotEmpty()) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, "Terdapat kendala di pemesanan", Toast.LENGTH_SHORT).show()
Log.e("CheckoutActivity", "Error from errorMessage: $message")
} }
} }
// Observe order creation // Observe order creation
viewModel.orderCreated.observe(this) { created -> viewModel.orderCreated.observe(this) { created ->
if (created) { if (created) {
Toast.makeText(this, "Order successfully created!", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Berhasil membuat pesanan", Toast.LENGTH_SHORT).show()
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
} }
} }
viewModel.productImages.observe(this) { images ->
Log.d("CheckoutActivity", "Product images updated: ${images.keys}")
// Update adapter when images arrive
cartCheckoutAdapter?.updateProductImages(images)
checkoutSellerAdapter?.updateProductImages(images)
}
} }
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<DetailPaymentItem>) { private fun setupPaymentMethodsRecyclerView(paymentMethods: List<DetailPaymentItem>) {
if (paymentMethods.isEmpty()) { if (paymentMethods.isEmpty()) {
Log.e("CheckoutActivity", "Payment methods list is empty") Log.e("CheckoutActivity", "Payment methods list is empty")
Toast.makeText(this, "No payment methods available", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Tidak ditemukan metode pembayaran", Toast.LENGTH_SHORT).show()
// Show empty payment state
binding.containerEmptyPayment.visibility = View.VISIBLE
binding.rvPaymentInfo.visibility = View.GONE
return return
} }
binding.containerEmptyPayment.visibility = View.GONE
binding.rvPaymentInfo.visibility = View.VISIBLE
// Debug logging // Debug logging
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available") Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
@ -237,17 +280,47 @@ class CheckoutActivity : AppCompatActivity() {
} }
private fun setupProductRecyclerView(checkoutData: CheckoutData) { private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) { if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData) Log.d("CheckoutActivity", "Using CheckoutSellerAdapter")
val adapter = CheckoutSellerAdapter(checkoutData)
// Keep reference for image updates - create a field in your activity
checkoutSellerAdapter = adapter
binding.rvProductItems.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
this.adapter = adapter
isNestedScrollingEnabled = false
}
// Load images for cart items
if (!checkoutData.isBuyNow) {
checkoutData.cartItems.forEach { item ->
viewModel.loadProductImage(item.productId)
}
}
} else { } else {
CartCheckoutAdapter(checkoutData) Log.d("CheckoutActivity", "Using CartCheckoutAdapter")
Log.d("CheckoutActivity", "Cart items count: ${checkoutData.cartItems.size}")
// Create adapter and keep reference
cartCheckoutAdapter = CartCheckoutAdapter(checkoutData)
binding.rvProductItems.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
adapter = cartCheckoutAdapter
isNestedScrollingEnabled = false
}
// Load images for each product
checkoutData.cartItems.forEach { item ->
Log.d("CheckoutActivity", "Loading image for productId: ${item.productId}")
viewModel.loadProductImage(item.productId)
}
} }
binding.rvProductItems.apply { binding.containerEmptyProducts.visibility = View.GONE
layoutManager = LinearLayoutManager(this@CheckoutActivity) binding.rvProductItems.visibility = View.VISIBLE
this.adapter = adapter
isNestedScrollingEnabled = false
}
} }
private fun updateOrderSummary() { private fun updateOrderSummary() {
@ -272,11 +345,18 @@ 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 // Hide empty state and show selected shipping
binding.containerEmptyShipping.visibility = View.GONE
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 {
// Show empty shipping state
binding.containerEmptyShipping.visibility = View.VISIBLE
binding.cardShipment.visibility = View.GONE
} }
} }
@ -288,10 +368,10 @@ class CheckoutActivity : AppCompatActivity() {
} }
// Shipping method selection // Shipping method selection
binding.layoutShippingMethod.setOnClickListener { binding.tvShippingOption.setOnClickListener {
val addressId = viewModel.addressDetails.value?.id ?: 0 val addressId = viewModel.addressDetails.value?.id ?: 0
if (addressId <= 0) { if (addressId <= 0) {
Toast.makeText(this, "Please select delivery address first", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Silahkan pilih alamat dahulu", Toast.LENGTH_SHORT).show()
return@setOnClickListener return@setOnClickListener
} }
@ -322,7 +402,16 @@ class CheckoutActivity : AppCompatActivity() {
// Create order button // Create order button
binding.btnPay.setOnClickListener { binding.btnPay.setOnClickListener {
if (validateOrder()) { if (validateOrder()) {
viewModel.createOrder() PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah anda yakin membuat pesanan?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.createOrder()
}
)
} }
} }
@ -341,7 +430,7 @@ class CheckoutActivity : AppCompatActivity() {
viewModel.setSelectedAddress(addressId) viewModel.setSelectedAddress(addressId)
// You might want to show a toast or some UI feedback // You might want to show a toast or some UI feedback
Toast.makeText(this, "Address selected successfully", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Berhasil memilih alamat", Toast.LENGTH_SHORT).show()
} }
} }
} }
@ -416,6 +505,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 +539,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

@ -11,31 +11,53 @@ import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
class CheckoutSellerAdapter(private val checkoutData: CheckoutData) : class CheckoutSellerAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CheckoutSellerAdapter.SellerViewHolder>() { RecyclerView.Adapter<CheckoutSellerAdapter.SellerViewHolder>() {
private var productImages: Map<Int, String> = emptyMap()
private var currentViewHolder: SellerViewHolder? = null
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root) class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate( val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context), parent, false LayoutInflater.from(parent.context), parent, false
) )
return SellerViewHolder(binding) val holder = SellerViewHolder(binding)
currentViewHolder = holder
return holder
} }
override fun getItemCount(): Int = 1 // Only one seller fun updateProductImages(newImages: Map<Int, String>) {
productImages = newImages
currentViewHolder?.let { holder ->
// Update the nested adapter
val adapter = holder.binding.rvSellerOrderProduct.adapter
when (adapter) {
is SingleCartItemAdapter -> adapter.updateProductImages(newImages)
is SingleProductAdapter -> {
// For SingleProductAdapter, you might need to update differently
// since it uses checkoutData.productImageUrl
}
}
}
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) { override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
currentViewHolder = holder
with(holder.binding) { with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView
rvSellerOrderProduct.apply { rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = if (checkoutData.isBuyNow) { adapter = if (checkoutData.isBuyNow) {
// Single product for Buy Now
SingleProductAdapter(checkoutData) SingleProductAdapter(checkoutData)
} else { } else {
// Single cart item SingleCartItemAdapter(checkoutData.cartItems.first()).also { adapter ->
SingleCartItemAdapter(checkoutData.cartItems.first()) // Apply existing images if available
if (productImages.isNotEmpty()) {
adapter.updateProductImages(productImages)
}
}
} }
isNestedScrollingEnabled = false isNestedScrollingEnabled = false
} }

View File

@ -40,7 +40,10 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCreated = MutableLiveData<Boolean>() private val _orderCreated = MutableLiveData<Boolean>()
val orderCreated: LiveData<Boolean> = _orderCreated val orderCreated: LiveData<Boolean> = _orderCreated
private val _productImages = MutableLiveData<Map<Int, String>>(emptyMap())
val productImages: LiveData<Map<Int, String>> = _productImages
private val currentImages = mutableMapOf<Int, String>()
// Initialize "Buy Now" checkout // Initialize "Buy Now" checkout
fun initializeBuyNow( fun initializeBuyNow(
@ -64,7 +67,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 +96,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 +145,32 @@ 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 Log.d(TAG, "Cek is reseller: ${orderRequest.isReseller}")
_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.forEach { item ->
Log.d("CheckoutViewModel", "About to load image for productId: ${item.productId}")
loadProductImage(item.productId)
}
matchingItems.forEach { item ->
loadProductImage(item.productId)
}
// Calculate totals with updated prices
calculateSubtotal() calculateSubtotal()
calculateTotal() calculateTotal()
} else { } else {
@ -151,12 +181,15 @@ 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
} }
} }
} }
fun getPaymentMethods() { fun getPaymentMethods() {
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -356,6 +389,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} else { } else {
// For Cart checkout, use the standard order endpoint // For Cart checkout, use the standard order endpoint
val cartRequest = data.orderRequest as OrderRequest val cartRequest = data.orderRequest as OrderRequest
Log.d(TAG, "data: ${cartRequest.cartItemId}")
repository.createOrder(cartRequest) repository.createOrder(cartRequest)
} }
@ -394,6 +428,31 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
fun loadProductImage(productId: Int) {
Log.d("CheckoutViewModel", "loadProductImage called for productId: $productId")
viewModelScope.launch {
try {
Log.d("CheckoutViewModel", "Fetching product detail for productId: $productId")
val productDetail = repository.fetchProductDetail(productId)
Log.d("CheckoutViewModel", "Product detail result: $productDetail")
val imageUrl = productDetail?.product?.image
Log.d("CheckoutViewModel", "Extracted image URL: $imageUrl")
currentImages[productId] = imageUrl.toString()
Log.d("CheckoutViewModel", "Updated currentImages: $currentImages")
_productImages.postValue(currentImages.toMap())
Log.d("CheckoutViewModel", "Posted to _productImages LiveData")
} catch (e: Exception) {
Log.e("CheckoutViewModel", "Error loading image for productId $productId", e)
// fallback if error
currentImages[productId] = ""
_productImages.postValue(currentImages.toMap())
}
}
}
// Get shipping price // Get shipping price
private fun getShippingPrice(): Double { private fun getShippingPrice(): Double {
val data = _checkoutData.value ?: return 0.0 val data = _checkoutData.value ?: return 0.0
@ -405,9 +464,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
companion object { companion object {
private const val TAG = "CheckoutViewModel" private const val TAG = "CheckoutViewModel"
} }
} }

View File

@ -67,7 +67,7 @@ class ShippingActivity : AppCompatActivity() {
// Validate required information // Validate required information
if (addressId <= 0 || productId <= 0) { if (addressId <= 0 || productId <= 0) {
Log.e(TAG, "Missing required shipping information: addressId=$addressId, productId=$productId") Log.e(TAG, "Missing required shipping information: addressId=$addressId, productId=$productId")
Toast.makeText(this, "Missing required shipping information", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gagal memuat pengiriman", Toast.LENGTH_SHORT).show()
finish() finish()
return return
} }

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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.cart.CartItemsItem import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding import com.alya.ecommerce_serang.databinding.ItemOrderProductBinding
@ -13,6 +14,8 @@ import java.util.Locale
class SingleCartItemAdapter(private val cartItem: CartItemsItem) : class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
RecyclerView.Adapter<SingleCartItemAdapter.CartItemViewHolder>() { RecyclerView.Adapter<SingleCartItemAdapter.CartItemViewHolder>() {
private var productImages: Map<Int, String> = emptyMap()
class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root) class CartItemViewHolder(val binding: ItemOrderProductBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
@ -24,16 +27,30 @@ class SingleCartItemAdapter(private val cartItem: CartItemsItem) :
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
fun updateProductImages(newImages: Map<Int, String>) {
productImages = newImages
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) { override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) {
with(holder.binding) { with(holder.binding) {
// Set cart item details
tvProductName.text = cartItem.productName tvProductName.text = cartItem.productName
tvProductQuantity.text = "${cartItem.quantity} buah" tvProductQuantity.text = "${cartItem.quantity} buah"
tvProductPrice.text = formatCurrency(cartItem.price.toDouble()) tvProductPrice.text = formatCurrency(cartItem.price.toDouble())
// Load placeholder image // Get the image for this product
val img = productImages[cartItem.productId]
val fullImageUrl = when (img) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> null
}
Glide.with(ivProduct.context) Glide.with(ivProduct.context)
.load(R.drawable.placeholder_image) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct) .into(ivProduct)
} }
} }

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
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.CheckoutData import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
@ -36,9 +37,16 @@ class SingleProductAdapter(private val checkoutData: CheckoutData) :
tvProductPrice.text = formatCurrency(checkoutData.productPrice) tvProductPrice.text = formatCurrency(checkoutData.productPrice)
val fullImageUrl = when (val img = checkoutData.productImageUrl) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> null
}
// Load product image // Load product image
Glide.with(ivProduct.context) Glide.with(ivProduct.context)
.load(checkoutData.productImageUrl) .load(fullImageUrl)
.apply( .apply(
RequestOptions() RequestOptions()
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)

View File

@ -22,23 +22,26 @@ 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
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
class AddAddressActivity : AppCompatActivity() { 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 +49,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 ->
@ -61,6 +66,12 @@ class AddAddressActivity : AppCompatActivity() {
binding = ActivityAddAddressBinding.inflate(layoutInflater) binding = ActivityAddAddressBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
applyLiveCounter(
binding.etDetailAlamat,
binding.tvCountDetail,
binding.tvCountDetailMax
)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager) apiService = ApiConfig.getApiService(sessionManager)
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
@ -80,11 +91,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 +109,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 +125,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 +145,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 +183,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 +241,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 +275,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 +294,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 +387,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 +441,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 +497,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 +525,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 +539,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 +557,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 +580,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 +599,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 +627,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

@ -52,12 +52,12 @@ class AddressActivity : AppCompatActivity() {
windowInsets windowInsets
} }
viewModel.fetchAddresses()
setupToolbar() setupToolbar()
setupRecyclerView() setupRecyclerView()
setupObservers() setupObservers()
viewModel.fetchAddresses()
} }
@ -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,12 @@ 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()
}
override fun onResume() {
super.onResume()
viewModel.fetchAddresses()
} }
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,12 @@ 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.R
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 +16,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 +33,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 +55,120 @@ 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 bankNames = context.resources.getStringArray(R.array.bank_names)
val defaultBanks = bankNames.map { BankItem(bankName = it) }
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

@ -33,13 +33,13 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody 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
@ -62,9 +62,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} }
private val paymentMethods = arrayOf( private val paymentMethods = arrayOf(
"Pilih Metode Pembayaran",
"Transfer Bank", "Transfer Bank",
"E-Wallet",
"QRIS",
) )
// private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> // private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
@ -124,12 +123,11 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "ERROR in AddEvidencePaymentActivity onCreate: ${e.message}", e) Log.e(TAG, "ERROR in AddEvidencePaymentActivity onCreate: ${e.message}", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
} }
} }
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
} }
@ -169,6 +167,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// Submit button // Submit button
binding.btnSubmit.setOnClickListener { binding.btnSubmit.setOnClickListener {
validateAndUpload() validateAndUpload()
Log.d(TAG, "AddEvidencePaymentActivity onCreate completed") Log.d(TAG, "AddEvidencePaymentActivity onCreate completed")
} }
@ -223,8 +222,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
val adapter = object : ArrayAdapter<String>(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) { val adapter = object : ArrayAdapter<String>(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent) val view = super.getView(position, convertView, parent)
val divider = view.findViewById<View>(R.id.divider)
divider.visibility = if (position == count - 1) View.GONE else View.VISIBLE
return view return view
} }
} }
@ -290,7 +287,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error handling selected image", e) Log.e(TAG, "Error handling selected image", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Terjadi kendala", Toast.LENGTH_SHORT).show()
} }
} }
@ -316,24 +313,35 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
return return
} }
if (binding.spinnerPaymentMethod.selectedItemPosition == 0) { //in case applied metode pembayaran yang lain
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show() // if (binding.spinnerPaymentMethod.selectedItemPosition == 0) {
return // Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
} // return
// }
binding.etAccountNumber.visibility = View.GONE
if (binding.etAccountNumber.text.toString().trim().isEmpty()) { //in case applied nomor rekening
Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show() // if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
return // Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
} // 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()
return // return
} // }
// All validations passed, proceed with upload // All validations passed, proceed with upload
uploadPaymentProof() PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah bukti yang dikirimkan sudah benar?",
message = "Pastikan bukti yang dikirimkan valid",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
uploadPaymentProof()
}
)
} }
private fun uploadPaymentProof() { private fun uploadPaymentProof() {
@ -368,7 +376,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
viewModel.uploadPaymentProof(request) viewModel.uploadPaymentProof(request)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error creating upload request: ${e.message}", e) Log.e(TAG, "Error creating upload request: ${e.message}", e)
Toast.makeText(this, "Error preparing upload: ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gagal mengunggah foto", Toast.LENGTH_SHORT).show()
} }
} }
} }
@ -445,9 +453,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
).show() ).show()
} }
companion object { companion object {
private const val PERMISSION_REQUEST_CODE = 100 private const val PERMISSION_REQUEST_CODE = 100
private const val TAG = "AddEvidenceActivity" private const val TAG = "AddEvidenceActivity"

View File

@ -160,7 +160,8 @@ class PaymentActivity : AppCompatActivity() {
viewModel.error.observe(this) { error -> viewModel.error.observe(this) { error ->
if (error.isNotEmpty()) { if (error.isNotEmpty()) {
Toast.makeText(this, error, Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gagal melakukan pembayaran", Toast.LENGTH_SHORT).show()
Log.e(TAG, "Failed payment: $error")
} }
} }
} }
@ -231,7 +232,6 @@ class PaymentActivity : AppCompatActivity() {
else -> emptyList() else -> emptyList()
} }
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type") .setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null) .setItems(instructions.toTypedArray(), null)

View File

@ -27,7 +27,6 @@ class HistoryActivity : AppCompatActivity() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHistoryBinding.inflate(layoutInflater) binding = ActivityHistoryBinding.inflate(layoutInflater)
@ -58,6 +57,14 @@ class HistoryActivity : AppCompatActivity() {
} }
} }
// override fun onDialogConfirmed() {
// // Option 1: refresh activity
// recreate()
//
// // Or Option 2: reload only data
// // viewModel.loadOrders()
// }
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)

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,9 +37,6 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
private const val TAG = "HistoryViewModel" private const val TAG = "HistoryViewModel"
} }
private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>()
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 +64,79 @@ 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() { private suspend fun getAllOrdersCombined(): ViewState<List<OrdersItem>> = try {
try { val statuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
val allOrders = mutableListOf<OrdersItem>()
// Use coroutineScope to allow launching async blocks val all = coroutineScope {
coroutineScope { statuses
val deferreds = allStatuses.map { status -> .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 +212,49 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
fun refreshOrders(status: String = "all") {
Log.d(TAG, "Refreshing orders with status: $status") fun updateStatus(status: String, forceRefresh: Boolean = false) {
// Don't set Loading here if you want to show current data while refreshing Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
getOrderList(status)
// 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) }
}
} }
fun refresh(status: String) {
Log.d(TAG, "⏳ refresh(\"$status\") started")
try {
viewModelScope.launch {
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

@ -28,10 +28,12 @@ import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import 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
@ -40,7 +42,8 @@ import java.util.TimeZone
class OrderHistoryAdapter( class OrderHistoryAdapter(
private val onOrderClickListener: (OrdersItem) -> Unit, private val onOrderClickListener: (OrdersItem) -> Unit,
private val viewModel: HistoryViewModel, private val viewModel: HistoryViewModel,
private val callbacks: OrderActionCallbacks private val callbacks: OrderActionCallbacks,
private val listener: OnDialogActionListener
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() { ) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
interface OrderActionCallbacks { interface OrderActionCallbacks {
@ -88,7 +91,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,13 +199,9 @@ 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 {
// visibility = View.VISIBLE
// text = formatDatePay(order.updatedAt)
// }
} }
"processed" -> { "processed" -> {
// Untuk status processed, tampilkan "Hubungi Penjual" // Untuk status processed, tampilkan "Hubungi Penjual"
@ -213,14 +213,6 @@ 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 {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
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 +229,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 {
@ -247,9 +239,18 @@ class OrderHistoryAdapter(
callbacks.onShowLoading(true) callbacks.onShowLoading(true)
// Call ViewModel // Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed") PopUpDialog.showConfirmDialog(
viewModel.refreshOrders() context = itemView.context,
title = "Apakah anda yakin pesanan sudah sampai?",
message = "Pastikan pesanan sudah samapi di alamat tujuan",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.confirmOrderCompleted(order.orderId, "completed")
listener.onDialogConfirmed()
}
)
// viewModel.refreshOrders()
} }
} }
@ -268,13 +269,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
@ -506,51 +515,54 @@ class OrderHistoryAdapter(
} else { } else {
// Log error and show a Toast instead if we can't get a FragmentManager // Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity") Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Terjadi kendala di batalkan pesanan", Toast.LENGTH_SHORT).show()
return return
} }
} }
else -> { else -> {
// Log error and show a Toast instead if we can't get a FragmentManager // Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity") Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Terjadi kendala di batalkan pesanan", Toast.LENGTH_SHORT).show()
return return
} }
} }
// 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 = {
callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Show a success message PopUpDialog.showConfirmDialog(
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show() context = itemView.context,
title = "Apakah anda yakin ingin membatalkan pesanan?",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Show a success message
Toast.makeText(context, "Pesanan berhasil dibatalkan", Toast.LENGTH_SHORT).show()
listener.onDialogConfirmed()
}
)
} }
) )
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)
// Create loading dialog
// val loadingDialog = Dialog(itemView.context).apply {
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// setContentView(R.layout.dialog_loading)
// window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// setCancelable(false)
// }
// loadingDialog.show()
viewModel.error.observe(itemView.findViewTreeLifecycleOwner()!!) { errorMsg -> viewModel.error.observe(itemView.findViewTreeLifecycleOwner()!!) { errorMsg ->
if (!errorMsg.isNullOrEmpty()) { if (!errorMsg.isNullOrEmpty()) {
Toast.makeText(itemView.context, errorMsg, Toast.LENGTH_SHORT).show() Log.e("OrderHistoryAdapter", "Error $errorMsg")
Toast.makeText(itemView.context, "Terdapat kendala di tambah ulasan", Toast.LENGTH_SHORT).show()
} }
} }
// 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
@ -582,13 +594,18 @@ class OrderHistoryAdapter(
} else { } else {
Toast.makeText( Toast.makeText(
itemView.context, itemView.context,
"No items to review", "Tidak ada produk untuk direview",
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} }
} }
private fun formatCurrency(amount: Double): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")
}
} }
companion object { companion object {
@ -606,4 +623,8 @@ class OrderHistoryAdapter(
} }
} }
} }
}
interface OnDialogActionListener {
fun onDialogConfirmed()
} }

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
@ -53,6 +64,24 @@ class OrderHistoryFragment : Fragment() {
else -> "Tab $position" else -> "Tab $position"
} }
}.attach() }.attach()
statusPage()
}
private fun statusPage(){
binding.viewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val status = viewPagerAdapter.orderStatuses[position]
historyVm.updateStatus(status, forceRefresh = true)
}
}
)
}
override fun onResume() {
super.onResume()
statusPage()
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -3,13 +3,16 @@ package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
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
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
@ -21,23 +24,32 @@ import com.alya.ecommerce_serang.ui.order.history.detailorder.DetailOrderStatusA
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
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks { class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks, OnDialogActionListener {
private var _binding: FragmentOrderListBinding? = null private var _binding: FragmentOrderListBinding? = null
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 +85,6 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
setupRecyclerView() setupRecyclerView()
observeOrderList() observeOrderList()
observeViewModel() observeViewModel()
observeOrderCompletionStatus()
loadOrders()
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
@ -83,7 +93,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
navigateToOrderDetail(order) navigateToOrderDetail(order)
}, },
viewModel = viewModel, viewModel = viewModel,
callbacks = this // Pass this fragment as callback callbacks = this,
listener = this// Pass this fragment as callback
) )
orderAdapter.setFragmentStatus(status) orderAdapter.setFragmentStatus(status)
@ -95,81 +106,66 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
} }
private fun observeOrderList() { private fun observeOrderList() {
// Now we only need to observe one LiveData for all cases viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.orders.observe(viewLifecycleOwner) { result -> viewModel.orders.collect { state ->
when (result) { when (state) {
is ViewState.Success -> { is ViewState.Loading -> {
binding.progressBar.visibility = View.GONE binding.progressBar.isVisible = true
}
if (result.data.isNullOrEmpty()) { is ViewState.Error -> {
binding.tvEmptyState.visibility = View.VISIBLE binding.progressBar.isVisible = false
binding.rvOrders.visibility = View.GONE binding.tvEmptyState.isVisible = true
} else { binding.rvOrders.isVisible = false
binding.tvEmptyState.visibility = View.GONE Log.e("OrderListFragment", "Error in order list: ${state.message}")
binding.rvOrders.visibility = View.VISIBLE }
orderAdapter.submitList(result.data) 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
} }
} }
} }
} }
private fun observeViewModel() { private fun observeViewModel() {
// Observe order completion
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 "Pesanan Selesai", Toast.LENGTH_SHORT).show()
} Log.d("OrderListFragment", "Order selesai")
is Result.Error -> { viewModel.updateStatus(status, forceRefresh = true)
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()
Log.e("OrderListFragment", "Failed: ${result.exception.message}")
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 "Pesanan Dibatalkan", Toast.LENGTH_SHORT).show()
} Log.d("OrderListFragment", "Order dibatalkan")
is Result.Error -> { viewModel.updateStatus(status, forceRefresh = true)
Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
}
is Result.Loading -> {
// Show loading if needed
} }
is Result.Error ->
Log.e("OrderListFragment", "Failed: ${result.exception.message}")
// Toast.makeText(requireContext(),
// "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
else -> { /* Loading */ }
} }
} }
} }
private fun loadOrders() {
// Simple - just call getOrderList for any status including "all"
viewModel.getOrderList(status)
}
private val detailOrderLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
// Refresh order list when returning with OK result
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 {
putExtra("ORDER_ID", order.orderId) putExtra("ORDER_ID", order.orderId)
@ -178,12 +174,12 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
detailOrderLauncher.launch(intent) detailOrderLauncher.launch(intent)
} }
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() Log.d("OrderListFragment", "Order cancel success: $message")
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()
} }
@ -191,10 +187,13 @@ 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(), "Pesanan selesai", Toast.LENGTH_SHORT).show()
loadOrders() // Refresh the list Log.d("OrderListFragment", "Pesanan selesai: $message")
// loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true)
} else { } else {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() Log.e("OrderListFragment", "Error Order Complete: $message")
Toast.makeText(requireContext(), "Terdapat kendala di pesanan selesai", Toast.LENGTH_SHORT).show()
} }
} }
@ -207,20 +206,18 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
_binding = null _binding = null
} }
private fun observeOrderCompletionStatus() { override fun onResume() {
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result -> super.onResume()
when (result) { observeOrderList()
is Result.Loading -> { }
// Handle loading state if needed
} override fun onDialogConfirmed() {
is Result.Success -> {
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show() viewModel.refresh(status)
loadOrders() // Option 1: refresh seluruh fragment
} requireActivity().supportFragmentManager.beginTransaction()
is Result.Error -> { .detach(this)
Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show() .attach(this)
} .commit()
}
}
} }
} }

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)
@ -67,7 +67,7 @@ class CancelOrderBottomSheet(
btnConfirm.setOnClickListener { btnConfirm.setOnClickListener {
if (selectedReason == null) { if (selectedReason == null) {
Toast.makeText(context, "Please select a reason", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Pilih alasan pembatalan", Toast.LENGTH_SHORT).show()
return@setOnClickListener return@setOnClickListener
} }
@ -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")
) )
} }
@ -130,13 +130,6 @@ class CancelOrderBottomSheet(
is Result.Success -> { is Result.Success -> {
// Hide loading indicator // Hide loading indicator
showLoading(false) showLoading(false)
// Show success message
Toast.makeText(
context,
"Pesanan berhasil dibatalkan",
Toast.LENGTH_SHORT
).show()
Log.d(TAG, "Cancel order status: SUCCESS, message: ${result.data.message}") Log.d(TAG, "Cancel order status: SUCCESS, message: ${result.data.message}")
// Notify callback and close dialog // Notify callback and close dialog

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")
@ -686,7 +742,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
inputFormat.timeZone = TimeZone.getTimeZone("UTC") inputFormat.timeZone = TimeZone.getTimeZone("UTC")
// Output format // Output format
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID")) val outputFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
// Parse the input date // Parse the input date
val date = inputFormat.parse(dateString) val date = inputFormat.parse(dateString)
@ -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

@ -90,7 +90,7 @@ class CreateReviewActivity : AppCompatActivity() {
) )
}) })
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(this, "Error loading review items", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Gagal memuat ulasan", Toast.LENGTH_SHORT).show()
finish() finish()
} }
} else { } else {
@ -110,7 +110,7 @@ class CreateReviewActivity : AppCompatActivity() {
) )
) )
} else { } else {
Toast.makeText(this, "No items to review", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Tidak ada produk untuk direview", Toast.LENGTH_SHORT).show()
finish() finish()
} }
} }

View File

@ -1,18 +1,26 @@
package com.alya.ecommerce_serang.ui.product package com.alya.ecommerce_serang.ui.product
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
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.view.ViewGroup
import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat import androidx.appcompat.widget.SwitchCompat
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
@ -36,6 +44,9 @@ import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
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.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -51,6 +62,8 @@ class DetailProductActivity : AppCompatActivity() {
private var isWholesaleSelected: Boolean = false private var isWholesaleSelected: Boolean = false
private var minOrder: Int = 0 private var minOrder: Int = 0
private var TAG = "DetailProductActivity"
private val viewModel: ProductUserViewModel by viewModels { private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -112,13 +125,17 @@ class DetailProductActivity : AppCompatActivity() {
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
updateStoreInfo(result.data) updateStoreInfo(result.data)
binding.progressBarDetailStore.visibility = View.GONE
} }
is Result.Error -> { is Result.Error -> {
// Show error message, maybe a Toast or Snackbar // Show error message, maybe a Toast or Snackbar
Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show() binding.progressBarDetailStore.visibility = View.GONE
Log.e("DetailProfileActivity", "Failed to load store: ${result.exception.message}")
Toast.makeText(this, "Kendala memuat toko", Toast.LENGTH_SHORT).show()
} }
is Result.Loading -> { is Result.Loading -> {
// Show loading indicator if needed // Show loading indicator if needed
binding.progressBarDetailStore.visibility = View.VISIBLE
} }
} }
} }
@ -160,10 +177,15 @@ class DetailProductActivity : AppCompatActivity() {
val products = viewModel.otherProducts.value.orEmpty() val products = viewModel.otherProducts.value.orEmpty()
if (products.isNotEmpty()) { if (products.isNotEmpty()) {
updateOtherProducts(products, storeMap) updateOtherProducts(products, storeMap)
} else {
binding.emptyOtherProducts.visibility = View.VISIBLE
binding.recyclerViewOtherProducts.visibility = View.GONE
binding.tvViewAllProducts.visibility = View.GONE
} }
} }
} }
//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
@ -189,12 +211,14 @@ class DetailProductActivity : AppCompatActivity() {
private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) { private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) { if (products.isEmpty()) {
Log.d("DetailProductActivity", "Product list is empty, hiding RecyclerView") Log.d("DetailProductActivity", "Product list is empty, hiding RecyclerView")
binding.recyclerViewOtherProducts.visibility = View.VISIBLE binding.recyclerViewOtherProducts.visibility = View.GONE
binding.emptyOtherProducts.visibility = View.VISIBLE
binding.tvViewAllProducts.visibility = View.GONE binding.tvViewAllProducts.visibility = View.GONE
} else { } else {
Log.d("DetailProductActivity", "Displaying product list in RecyclerView") Log.d("DetailProductActivity", "Displaying product list in RecyclerView")
binding.recyclerViewOtherProducts.visibility = View.VISIBLE binding.recyclerViewOtherProducts.visibility = View.VISIBLE
binding.tvViewAllProducts.visibility = View.VISIBLE binding.tvViewAllProducts.visibility = View.VISIBLE
binding.emptyOtherProducts.visibility = View.GONE
productAdapter = OtherProductAdapter(products, onClick = { product -> productAdapter = OtherProductAdapter(products, onClick = { product ->
handleProductClick(product) handleProductClick(product)
@ -230,9 +254,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 +266,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 +304,27 @@ class DetailProductActivity : AppCompatActivity() {
.load(fullImageUrl) .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage) .into(binding.ivProductImage)
binding.ivProductImage.setOnClickListener {
val img = product.image
if (!img.isNullOrEmpty()){
showDetailProduct(img)
}else {
Toast.makeText(this, "Gambar tidak tersedia", Toast.LENGTH_SHORT).show()
Log.e(TAG, "There is no photo product")
}
}
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) {
@ -305,11 +349,13 @@ class DetailProductActivity : AppCompatActivity() {
val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList() val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList()
if (reviewList.isEmpty()) { if (reviewList.isEmpty()) {
binding.recyclerViewReviews.visibility = View.GONE binding.recyclerViewReviews.visibility = View.GONE
binding.emptyReview.visibility = View.VISIBLE
binding.tvViewAllReviews.visibility = View.GONE binding.tvViewAllReviews.visibility = View.GONE
// binding.tvNoReviews.visibility = View.VISIBLE // binding.tvNoReviews.visibility = View.VISIBLE
} else { } else {
binding.recyclerViewReviews.visibility = View.VISIBLE binding.recyclerViewReviews.visibility = View.VISIBLE
binding.tvViewAllReviews.visibility = View.VISIBLE binding.tvViewAllReviews.visibility = View.VISIBLE
binding.emptyReview.visibility = View.GONE
} }
// binding.tvNoReviews.visibility = View.GONE // binding.tvNoReviews.visibility = View.GONE
reviewsAdapter = ReviewsAdapter( reviewsAdapter = ReviewsAdapter(
@ -347,6 +393,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 +424,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
@ -508,7 +554,64 @@ class DetailProductActivity : AppCompatActivity() {
attachProduct = true // This will auto-attach the product! attachProduct = true // This will auto-attach the product!
) )
}
private fun showDetailProduct(photoProduct: String) {
val dialog = Dialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.dialog_image_viewer)
dialog.setCancelable(true)
// Set dialog to fullscreen
val window = dialog.window
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
window?.setBackgroundDrawable(Color.WHITE.toDrawable())
// Get views from dialog
val imageView = dialog.findViewById<ImageView>(R.id.iv_payment_evidence)
val btnClose = dialog.findViewById<ImageButton>(R.id.btn_close)
val tvTitle = dialog.findViewById<TextView>(R.id.tv_title)
val progressBar = dialog.findViewById<ProgressBar>(R.id.progress_bar)
tvTitle.text = "Gambar Produk"
val fullImageUrl =
if (photoProduct.startsWith("/")) BASE_URL + photoProduct.substring(1)
else photoProduct
progressBar.visibility = View.VISIBLE
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(resource)
}
override fun onLoadCleared(placeholder: Drawable?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(placeholder)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
progressBar.visibility = View.GONE
imageView.setImageDrawable(errorDrawable)
Toast.makeText(this@DetailProductActivity, "Gagal memuat gambar", Toast.LENGTH_SHORT).show()
}
})
btnClose.setOnClickListener { dialog.dismiss() }
imageView.setOnClickListener { dialog.dismiss() }
dialog.show()
}
override fun onResume() {
super.onResume()
loadData()
} }
companion object { companion object {

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

@ -88,13 +88,14 @@ class CategoryProductsActivity : AppCompatActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.apply { supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
// title = category.name title = ""
} }
val fullImageUrl = if (category.image.startsWith("/")) { val fullImageUrl = when (val img = category.image) {
BASE_URL + category.image.removePrefix("/") // Append base URL if the path starts with "/" is String -> {
} else { if (img.startsWith("/")) BASE_URL + img.substring(1) else img
category.image // Use as is if it's already a full URL }
else -> null
} }
// Load category image // Load category image

View File

@ -3,7 +3,7 @@ package com.alya.ecommerce_serang.ui.product.category
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig 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.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
@ -46,8 +46,15 @@ class ProductsCategoryAdapter(
val priceValue = product.price.toDoubleOrNull() ?: 0.0 val priceValue = product.price.toDoubleOrNull() ?: 0.0
tvProductPrice.text = "Rp ${NumberFormat.getNumberInstance(Locale("id", "ID")).format(priceValue.toInt())}" tvProductPrice.text = "Rp ${NumberFormat.getNumberInstance(Locale("id", "ID")).format(priceValue.toInt())}"
// Load product image // Load product image
val fullImageUrl = when (val img = product.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> null
}
Glide.with(itemView.context) Glide.with(itemView.context)
.load("${BuildConfig.BASE_URL}${product.image}") .load(fullImageUrl)
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image) .error(R.drawable.placeholder_image)
.centerCrop() .centerCrop()
@ -57,15 +64,6 @@ class ProductsCategoryAdapter(
root.setOnClickListener { root.setOnClickListener {
onClick(product) onClick(product)
} }
// // Optional: Show stock status
// if (product.stock > 0) {
// tvStockStatus.text = "Stock: ${product.stock}"
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.green))
// } else {
// tvStockStatus.text = "Out of Stock"
// tvStockStatus.setTextColor(ContextCompat.getColor(itemView.context, R.color.red))
// }
} }
} }
} }

View File

@ -64,10 +64,9 @@ class StoreDetailActivity : AppCompatActivity() {
) )
windowInsets windowInsets
} }
loadData()
setupUI() setupUI()
setupObservers() setupObservers()
loadData()
} }
private fun setupUI() { private fun setupUI() {
@ -88,15 +87,18 @@ class StoreDetailActivity : AppCompatActivity() {
viewModel.storeDetail.observe(this) { result -> viewModel.storeDetail.observe(this) { result ->
when (result) { when (result) {
is Result.Success -> { is Result.Success -> {
binding.progressBarDetailProdItem.visibility = View.GONE
updateStoreInfo(result.data) updateStoreInfo(result.data)
viewModel.loadOtherProducts(result.data.storeId) viewModel.loadOtherProducts(result.data.storeId)
} }
is Result.Error -> { is Result.Error -> {
// Show error message, maybe a Toast or Snackbar // Show error message, maybe a Toast or Snackbar
binding.progressBarDetailProdItem.visibility = View.GONE
Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show()
} }
is Result.Loading -> { is Result.Loading -> {
// Show loading indicator if needed // Show loading indicator if needed
binding.progressBarDetailProdItem.visibility = View.VISIBLE
} }
} }
} }
@ -109,6 +111,9 @@ class StoreDetailActivity : AppCompatActivity() {
val products = viewModel.otherProducts.value.orEmpty() val products = viewModel.otherProducts.value.orEmpty()
if (products.isNotEmpty()) { if (products.isNotEmpty()) {
updateProducts(products, storeMap) updateProducts(products, storeMap)
} else {
binding.progressBarDetailProdItem.visibility = View.VISIBLE
binding.rvProducts.visibility = View.GONE
} }
} }
} }
@ -128,7 +133,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,16 +149,29 @@ 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() ?: 0f
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)
}
} }
} }
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) { private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) { if (products.isEmpty()) {
binding.rvProducts.visibility = View.GONE binding.rvProducts.visibility = View.GONE
binding.progressBarDetailProdItem.visibility = View.VISIBLE
Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView") Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView")
} else { } else {
Log.d("StoreDetailActivity", "Displaying product list in RecyclerView") Log.d("StoreDetailActivity", "Displaying product list in RecyclerView")
binding.progressBarDetailProdItem.visibility = View.GONE
binding.rvProducts.visibility = View.VISIBLE binding.rvProducts.visibility = View.VISIBLE
productAdapter = HorizontalProductAdapter(products, onClick = { product -> productAdapter = HorizontalProductAdapter(products, onClick = { product ->
handleProductClick(product) handleProductClick(product)

View File

@ -45,11 +45,11 @@ class StoreDetailViewModel (private val repository: ProductRepository
} // Filter by storeId and exclude current product } // Filter by storeId and exclude current product
_otherProducts.value = filteredProducts // Update LiveData _otherProducts.value = filteredProducts // Update LiveData
} else if (result is Result.Error) { } else if (result is Result.Error) {
Log.e("ProductViewModel", "Error loading other products: ${result.exception.message}") Log.e("StoreDetailViewModel", "Error loading other products: ${result.exception.message}")
_otherProducts.value = emptyList() // Set empty list on failure _otherProducts.value = emptyList() // Set empty list on failure
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ProductViewModel", "Exception loading other products: ${e.message}") Log.e("StoreDetailViewModel", "Exception loading other products: ${e.message}")
_otherProducts.value = emptyList() _otherProducts.value = emptyList()
} }
} }
@ -67,7 +67,7 @@ class StoreDetailViewModel (private val repository: ProductRepository
loadStoreDetail(storeId) loadStoreDetail(storeId)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ProductViewModel", "Error loading product details: ${e.message}") Log.e("StoreDetailViewModel", "Error loading product details: ${e.message}")
_error.value = "Failed to load product details: ${e.message}" _error.value = "Failed to load product details: ${e.message}"
} finally { } finally {
_isLoading.value = false _isLoading.value = false
@ -82,7 +82,7 @@ class StoreDetailViewModel (private val repository: ProductRepository
val result = repository.fetchStoreDetail(storeId) val result = repository.fetchStoreDetail(storeId)
_storeDetail.value = result _storeDetail.value = result
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ProductViewModel", "Error loading store details: ${e.message}") Log.e("StoreDetailViewModel", "Error loading store details: ${e.message}")
_storeDetail.value = Result.Error(e) _storeDetail.value = Result.Error(e)
} }
} }
@ -99,10 +99,10 @@ class StoreDetailViewModel (private val repository: ProductRepository
if (result is Result.Success) { if (result is Result.Success) {
map[storeId] = result.data map[storeId] = result.data
} else if (result is Result.Error) { } else if (result is Result.Error) {
Log.e("ProductViewModel", "Failed to load storeId $storeId", result.exception) Log.e("StoreDetailViewModel", "Failed to load storeId $storeId", result.exception)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("ProductViewModel", "Exception fetching storeId $storeId", e) Log.e("StoreDetailViewModel", "Exception fetching storeId $storeId", e)
} }
} }

View File

@ -0,0 +1,79 @@
package com.alya.ecommerce_serang.ui.profile
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityChangePasswordBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import kotlin.getValue
class ChangePasswordActivity : AppCompatActivity() {
private lateinit var binding: ActivityChangePasswordBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ProfileViewModel(userRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
binding.header.headerTitle.text = "Ubah Kata Sandi"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
// Listen for the result of the password change
viewModel.changePasswordResult.observe(this, Observer { result ->
when (result) {
is Result.Error -> {
Toast.makeText(
this,
"Gagal mengubah kata sandi: ${result.exception.message}",
Toast.LENGTH_SHORT
).show()
}
is Result.Success -> {
Toast.makeText(this, "Berhasil mengubah kata sandi", Toast.LENGTH_SHORT).show()
finish()
}
is Result.Loading -> {}
}
})
// Button to trigger password change
binding.btnChangePass.setOnClickListener {
val currentPassword = binding.etLoginPassword.text.toString()
val newPassword = binding.etLoginNewPassword.text.toString()
if (currentPassword.isNotEmpty() && newPassword.isNotEmpty()) {
// Call change password function from ViewModel
viewModel.changePassword(currentPassword, newPassword)
} else {
Toast.makeText(this, "Lengkapi data", Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@ -90,6 +90,8 @@ class DetailProfileActivity : AppCompatActivity() {
Log.e("DetailProfileActivity", "Error from ViewModel: $error") Log.e("DetailProfileActivity", "Error from ViewModel: $error")
Toast.makeText(this, error, Toast.LENGTH_SHORT).show() Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
} }
} }
private fun setupClickListeners() { private fun setupClickListeners() {
@ -106,7 +108,8 @@ class DetailProfileActivity : AppCompatActivity() {
} }
editProfileLauncher.launch(intent) editProfileLauncher.launch(intent)
} ?: run { } ?: run {
Toast.makeText(this, "Profile data is not available", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Akun tidak ditemukan", Toast.LENGTH_SHORT).show()
Log.e("DetailProfileActivity", "Profile data is not available")
} }
} }
} }

View File

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.app.AlertDialog
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -16,15 +15,20 @@ 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.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.RegisterStoreActivity
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.PopUpDialog
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,9 +48,16 @@ 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())
} }
override fun onCreateView( override fun onCreateView(
@ -59,26 +70,66 @@ 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()
sessionManager = SessionManager(requireContext())
if (!sessionManager.isLoggedIn()) {
// Redirect to LoginActivity
binding.tvName.text = "Selamat Datang"
binding.tvUsername.text = "Silahkan masuk"
binding.btnDetailProfile.text = "Masuk"
binding.btnDetailProfile.setOnClickListener {
val intent = Intent(requireContext(), LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
// ✅ Finish the host activity so user cant go back
requireActivity().finish()
}
binding.containerBukaToko.visibility = View.GONE
binding.cardPesanan.visibility = View.GONE
binding.tvPengaturanAkun.visibility = View.GONE
binding.containerSettings.visibility = View.GONE
binding.cardAbout.visibility = View.GONE
binding.cardLogout.visibility = View.GONE
}
viewModel.loadUserProfile() viewModel.loadUserProfile()
viewModel.checkStoreUser() viewModel.checkStoreUser()
observeUserProfile()
observeStoreStatus()
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) { storeDataResponse ->
binding.tvBukaToko.text = "Lihat Toko Saya" storeDataResponse?.let { storeResponse ->
val intentBuka = Intent(requireContext(), MyStoreActivity::class.java) val store = storeResponse.store
startActivity(intentBuka) when (store.approvalStatus) {
} else { "process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java))
binding.tvBukaToko.text = "Buka Toko" "rejected" -> startActivity(
val intentBuka = Intent(requireContext(), RegisterStoreActivity::class.java) Intent(requireContext(), RegisterStoreActivity::class.java)
startActivity(intentBuka) .putExtra("REAPPLY", true)
} )
else -> {
when(store.storeStatus){
"suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java))
else -> startActivity(Intent(requireContext(), MyStoreActivity::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 +147,19 @@ class ProfileFragment : Fragment() {
startActivity(intent) startActivity(intent)
} }
binding.cardLogout.setOnClickListener({ binding.cardChangePass.setOnClickListener{
val intent = Intent(requireContext(), ChangePasswordActivity::class.java)
startActivity(intent)
}
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() {
@ -112,7 +167,14 @@ class ProfileFragment : Fragment() {
user?.let { updateUI(it) } user?.let { updateUI(it) }
} }
viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage -> viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage ->
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show() // Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
Log.e("Profile Fragment", "Failed to load profile: $errorMessage")
}
}
private fun observeStoreStatus() {
viewModel.checkStore.observe(viewLifecycleOwner) { hasStore ->
binding.tvBukaToko.text = if (hasStore) "Toko Saya" else "Buka Toko"
} }
} }
@ -138,14 +200,16 @@ class ProfileFragment : Fragment() {
private fun logout(){ private fun logout(){
AlertDialog.Builder(requireContext()) PopUpDialog.showConfirmDialog(
.setTitle("Konfirmasi") context = requireContext(),
.setMessage("Apakah anda yakin ingin keluar?") title = "Konfirmasi",
.setPositiveButton("Ya") { _, _ -> message = "Apakah anda yakin ingin keluar?",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
actionLogout() actionLogout()
} }
.setNegativeButton("Tidak", null) )
.show()
} }
private fun actionLogout(){ private fun actionLogout(){
@ -160,8 +224,11 @@ class ProfileFragment : Fragment() {
delay(500) delay(500)
loadingDialog.dismiss() loadingDialog.dismiss()
sessionManager.clearAll() sessionManager.clearAll()
viewModel.deleteFCM()
val intent = Intent(requireContext(), LoginActivity::class.java) val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent) startActivity(intent)
requireActivity().finish()
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(),
@ -172,4 +239,11 @@ class ProfileFragment : Fragment() {
} }
} }
override fun onResume() {
super.onResume()
viewModel.loadUserProfile()
viewModel.checkStoreUser()
}
} }

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