Compare commits

43 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
155 changed files with 5823 additions and 1709 deletions

View File

@ -4,14 +4,27 @@
<selectionStates>
<SelectionState runConfigName="app">
<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">
<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>
</Target>
</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>
</selectionStates>
</component>

View File

@ -5,7 +5,7 @@
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
<option name="order" value="DESCENDING" />
</ColumnSorterState>
</list>
</option>

View File

@ -1,4 +1,5 @@
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
@ -124,4 +125,11 @@ dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics")
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

@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.profile.ChangePasswordActivity"
android:exported="false" />
<activity
android:name=".ui.auth.ResetPassActivity"
android:exported="false" />
@ -170,7 +173,8 @@
<activity
android:name=".ui.auth.RegisterActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen">
<intent-filter>
<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

@ -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

@ -10,9 +10,6 @@ data class Store(
@field:SerializedName("store_status")
val storeStatus: String,
@field:SerializedName("sppirt")
val sppirt: String,
@field:SerializedName("user_name")
val userName: String,
@ -37,9 +34,6 @@ data class Store(
@field:SerializedName("user_phone")
val userPhone: String,
@field:SerializedName("halal")
val halal: String,
@field:SerializedName("id")
val id: Int,

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

@ -62,6 +62,9 @@ data class Product(
@field:SerializedName("wholesale_min_item")
val wholesaleMinItem: Int? = null,
@field:SerializedName("status")
val status: String,
@field:SerializedName("min_order")
val minOrder: Int,
@ -82,7 +85,8 @@ data class Product(
data class CartItemCheckoutInfo(
val cartItem: CartItemsItem,
val isWholesale: Boolean
val isWholesale: Boolean,
val wholesalePrice: Int? = null
)

View File

@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class AddressDetailResponse(
@field:SerializedName("address")
val address: Address,
val address: AddressDetail,
@field:SerializedName("message")
val message: String
@ -14,7 +14,7 @@ data class AddressDetailResponse(
data class AddressDetail(
@field:SerializedName("village_id")
val villageId: String,
val villageId: String?,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@ -23,13 +23,13 @@ data class AddressDetail(
val latitude: String,
@field:SerializedName("province_name")
val provinceName: String,
val provinceName: String?,
@field:SerializedName("subdistrict_id")
val subdistrictId: String,
val subdistrictId: String?,
@field:SerializedName("city_name")
val cityName: String,
val cityName: String?,
@field:SerializedName("user_id")
val userId: Int,
@ -38,7 +38,7 @@ data class AddressDetail(
val provinceId: String,
@field:SerializedName("phone")
val phone: String,
val phone: String?,
@field:SerializedName("street")
val street: String,
@ -47,7 +47,7 @@ data class AddressDetail(
val subdistrict: String,
@field:SerializedName("recipient")
val recipient: String,
val recipient: String?,
@field:SerializedName("id")
val id: Int,

View File

@ -1,18 +1,13 @@
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
data class StoreResponse(
val message: String,
val store: Store
)
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
val store: Store,
val shipping: List<Shipping> = emptyList(),
val payment: List<Payment> = emptyList()
)

View File

@ -1,10 +1,10 @@
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.CancelOrderReq
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.CompletedOrderRequest
import com.alya.ecommerce_serang.data.api.dto.ConfirmPaymentRequest
@ -16,7 +16,6 @@ import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.PaymentConfirmRequest
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.ResetPassReq
@ -27,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.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.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.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
@ -63,6 +63,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreP
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.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.CreateAddressResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
@ -80,12 +81,10 @@ import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductRe
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
@ -96,7 +95,6 @@ import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService {
@POST("registeruser")
@ -109,8 +107,9 @@ interface ApiService {
@Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse
@GET("checkstore")
suspend fun checkStore (): Response<CheckStoreResponse>
@PUT("deletefcm")
suspend fun deleteFCMToken (
): DeleteFCMResponse
@Multipart
@POST("registerstore")
@ -201,11 +200,6 @@ interface ApiService {
@Path("id") orderId: Int
): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart
@POST("order/addevidence")
suspend fun addEvidenceMultipart(
@ -253,15 +247,9 @@ interface ApiService {
@GET("mystore")
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
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@GET("category")
fun getCategories(): Call<CategoryResponse>
@Multipart
@POST("store/createproduct")
suspend fun addProduct(
@ -371,9 +359,6 @@ interface ApiService {
@GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart
@POST("store/createtopup")
suspend fun addBalanceTopUp(
@ -385,11 +370,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse>
@PUT("store/payment/update")
suspend fun paymentConfirmation(
@Body confirmPaymentReq : PaymentConfirmRequest
): Response<PaymentConfirmationResponse>
@Multipart
@PUT("mystore/edit")
suspend fun updateStoreProfileMultipart(
@ -401,13 +381,26 @@ interface ApiService {
): Response<StoreDataResponse>
@Multipart
@POST("mystore/payment/add")
suspend fun addPaymentInfo(
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part qris: MultipartBody.Part?
): Response<GenericResponse>
@PUT("mystore/edit")
suspend fun updateStoreApprovalMultipart(
@Part("store_name") storeName: RequestBody,
@Part("store_description") storeDescription: RequestBody,
@Part("store_type_id") storeTypeId: RequestBody,
@Part("latitude") storeLatitude: RequestBody,
@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
@POST("mystore/payment/add")
@ -418,6 +411,16 @@ interface ApiService {
@Part qris: MultipartBody.Part?
): 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}")
suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int
@ -461,15 +464,6 @@ interface ApiService {
@GET("search")
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
@POST("store/sendchat")
suspend fun sendChatMessageStore(
@ -528,4 +522,14 @@ interface ApiService {
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,27 +1,33 @@
package com.alya.ecommerce_serang.data.repository
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.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.sells.OrderListResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException
import retrofit2.Response
import java.io.File
import java.io.IOException
class MyStoreRepository(private val apiService: ApiService) {
suspend fun fetchMyStoreProfile(): Result<Store?> {
suspend fun fetchMyStoreProfile(): Result<StoreResponse?> {
return try {
val response = apiService.getStore()
val response = apiService.getMyStoreData()
if (response.isSuccessful) {
val storeResponse: StoreResponse? = response.body()
Result.Success(storeResponse?.store)
val storeResponse = response.body()
Result.Success(storeResponse)
} else {
val errorMessage = response.errorBody()?.string() ?: "Unknown API error"
Log.e("MyStoreRepository", "Error: $errorMessage")
@ -139,6 +145,186 @@ class MyStoreRepository(private val apiService: ApiService) {
}
}
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"
}

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.OrderDetailResponse
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.ProductResponse
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.profile.AddressDetailResponse
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.UpdateAddressResponse
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
@ -294,6 +298,43 @@ class OrderRepository(private val apiService: ApiService) {
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?> {
return try {
val response = apiService.getUserProfile()
@ -319,6 +360,7 @@ class OrderRepository(private val apiService: ApiService) {
}
}
suspend fun uploadPaymentProof(request: AddEvidenceMultipartRequest): Result<AddEvidenceResponse> {
return try {
Log.d("OrderRepository", "Uploading payment proof...")

View File

@ -194,7 +194,7 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) {
Result.Success(response.body()!!)
} 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) {
Result.Error(e)

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.data.repository
import android.content.Context
import android.net.Uri
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.ChangePasswordRequest
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.OtpRequest
@ -10,6 +11,8 @@ 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.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.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
@ -516,6 +519,33 @@ class UserRepository(private val apiService: ApiService) {
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{
private const val TAG = "UserRepository"
}

View File

@ -5,10 +5,9 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
@ -16,6 +15,7 @@ import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityLoginBinding
import com.alya.ecommerce_serang.ui.MainActivity
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.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp
@ -39,31 +39,14 @@ class LoginActivity : AppCompatActivity() {
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
// ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
// val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
// view.setPadding(
// systemBars.left,
// systemBars.top,
// systemBars.right,
// systemBars.bottom
// )
// windowInsets
// }
// onBackPressedDispatcher.addCallback(this) {
// // Handle the back button event
// }
//
// WindowCompat.setDecorFitsSystemWindows(window, false)
// enableEdgeToEdge()
setupListeners()
observeLoginState()
FirebaseApp.initializeApp(this)
// Request FCM token at app startup
}
private fun setupListeners() {
@ -72,7 +55,7 @@ class LoginActivity : AppCompatActivity() {
val password = binding.etLoginPassword.text.toString()
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 {
loginViewModel.login(email, password)
}
@ -100,14 +83,24 @@ class LoginActivity : AppCompatActivity() {
retrieveFCMToken()
// 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))
finish()
}
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show()
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.ic_cancel,
title = "Gagal Masuk"
)
Toast.makeText(this, "Gagal masuk", Toast.LENGTH_LONG).show()
}
is Result.Loading -> {
// Show loading state

View File

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

View File

@ -6,6 +6,7 @@ import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -39,6 +40,8 @@ class RegisterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Thread.sleep(3000)
installSplashScreen()
binding = ActivityRegisterBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
@ -86,20 +89,72 @@ class RegisterActivity : AppCompatActivity() {
}
}
// navigate step register in fragment
// In RegisterActivity, add debug to navigateToStep:
fun navigateToStep(step: Int, userData: RegisterRequest?) {
val fragment = when (step) {
1 -> RegisterStep1Fragment.newInstance()
2 -> RegisterStep2Fragment.newInstance(userData)
3 -> RegisterStep3Fragment.newInstance()
else -> null
Log.d("RegisterActivity", "=== NAVIGATE TO STEP START ===")
Log.d("RegisterActivity", "Target step: $step")
Log.d("RegisterActivity", "Current fragment count: ${supportFragmentManager.fragments.size}")
Log.d("RegisterActivity", "UserData: ${userData?.email}")
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()
.replace(R.id.fragment_container, it)
.addToBackStack(null)
.commit()
try {
val fragment = when (step) {
1 -> {
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

@ -5,13 +5,14 @@ import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.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() {
@ -36,7 +37,7 @@ class ResetPassActivity : AppCompatActivity() {
setupToolbar()
setupUI()
observeResetPassword()
}
private fun setupToolbar(){
@ -98,26 +99,23 @@ class ResetPassActivity : AppCompatActivity() {
private fun handleSuccess(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
// Show success dialog and navigate back to login
AlertDialog.Builder(this)
.setTitle("Berhasil Ubah Password")
.setMessage(message)
.setPositiveButton("OK") { _, _ ->
// Navigate back to login activity
finish()
}
.setCancelable(false)
.show()
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.checkmark__1_,
title = "Berhasil Ubah Password",
positiveText = "OK"
)
}
private fun handleError(errorMessage: String) {
Toast.makeText(this, "Error: $errorMessage", Toast.LENGTH_LONG).show()
Log.e(TAG, "Error: $errorMessage")
// Optionally show error dialog
AlertDialog.Builder(this)
.setTitle("Gagal Ubah Password")
.setMessage(errorMessage)
.setPositiveButton("OK", null)
.show()
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" -> {
isEmailValid = 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" -> {
isPhoneValid = 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 -> {
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,7 +201,8 @@ class RegisterStep1Fragment : Fragment() {
is Result.Error -> {
binding.progressBar.visibility = View.GONE
binding.btnNext.isEnabled = true
Toast.makeText(requireContext(), "OTP Request Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
Log.e(TAG, "OTP Request Failed: ${result.exception.message}")
Toast.makeText(requireContext(), "Gagal mendapatkan OTP. Kirim ulang OTP", Toast.LENGTH_SHORT).show()
}
}
}
@ -229,13 +231,13 @@ class RegisterStep1Fragment : Fragment() {
// Check if all fields are filled
if (email.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() || phone.isEmpty() ||
username.isEmpty() || fullName.isEmpty() || birthDate.isEmpty()) {
Toast.makeText(requireContext(), "Please fill all required fields", Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Silahkan lengkapi seluruh isian", Toast.LENGTH_SHORT).show()
return
}
// Check if passwords match
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
}
@ -253,7 +255,7 @@ class RegisterStep1Fragment : Fragment() {
if (isEmailValid && isPhoneValid) {
requestOtp(email)
} 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
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@ -13,6 +15,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -24,11 +27,15 @@ import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.firebase.messaging.FirebaseMessaging
class RegisterStep2Fragment : Fragment() {
private var _binding: FragmentRegisterStep2Binding? = null
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private var countDownTimer: CountDownTimer? = null
private var timeRemaining = 30
private var isTimerRunning = false
// In RegisterStep2Fragment AND RegisterStep3Fragment:
private val registerViewModel: RegisterViewModel by activityViewModels {
@ -39,8 +46,8 @@ class RegisterStep2Fragment : Fragment() {
RegisterViewModel(userRepository, orderRepository, requireContext())
}
}
private var countDownTimer: CountDownTimer? = null
private var timeRemaining = 30 // 30 seconds cooldown for resend
// private var countDownTimer: CountDownTimer? = null
// private var timeRemaining = 30 // 30 seconds cooldown for resend
companion object {
@ -112,6 +119,20 @@ class RegisterStep2Fragment : Fragment() {
observeRegistrationState()
observeLoginState()
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?) {
@ -129,11 +150,6 @@ class RegisterStep2Fragment : Fragment() {
Log.d(TAG, "Updating user data with OTP: $otp")
registerViewModel.updateUserData(updatedUserData)
// For demo purposes, we're just proceeding to Step 3
// In a real app, you would verify the OTP with the server first
// registerViewModel.setStep(3)
// (activity as? RegisterActivity)?.navigateToStep(3, updatedUserData)
registerViewModel.registerUser(updatedUserData)
} ?: Log.e(TAG, "userData is null, cannot proceed with verification")
}
@ -170,37 +186,9 @@ class RegisterStep2Fragment : Fragment() {
} ?: 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 = "Kirim ulang OTP dalam waktu 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 = "Dapat mengirim ulang kode OTP"
binding.tvResendOtp.isEnabled = true
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
timeRemaining = 0
}
}.start()
}
private fun observeRegistrationState() {
registerViewModel.message.observe(viewLifecycleOwner) { message ->
Log.d(TAG, "Message from server: $message")
// You can use the message here if needed, e.g., for showing in a specific UI element
// or for storing for later use
}
registerViewModel.registerState.observe(viewLifecycleOwner) { result ->
when (result) {
@ -250,6 +238,8 @@ class RegisterStep2Fragment : Fragment() {
// Save the token in fragment
val accessToken = result.data.accessToken
sessionManager.saveToken(accessToken)
retrieveFCMToken()
Log.d(TAG, "Token saved to SessionManager: $accessToken")
// Proceed to Step 3
@ -279,9 +269,116 @@ class RegisterStep2Fragment : Fragment() {
}
}
override fun onDestroyView() {
super.onDestroyView()
private fun retrieveFCMToken() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (!task.isSuccessful) {
Log.e(TAG, "Failed to get FCM token", task.exception)
return@addOnCompleteListener
}
val token = task.result
// tokenTes = token
Log.d(TAG, "FCM token retrieved: $token")
// Save token locally
val sharedPreferences = 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 = 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
}
override fun onDetach() {
super.onDetach()
Log.d(TAG, "onDetach - final cleanup")
stopTimer()
}
}

View File

@ -27,6 +27,7 @@ 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.VillagesAdapter
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.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator
@ -96,26 +97,30 @@ class RegisterStep3Fragment : Fragment() {
Log.d(TAG, "Auto-filled name: ${it.name}, phone: ${it.phone}")
}
// Set up province and city dropdowns
setupAutoComplete()
setupEdgeToEdge()
// Set up button listeners
binding.btnPrevious.setOnClickListener {
// Go back to the previous step
parentFragmentManager.popBackStack()
val step2Fragment = RegisterStep2Fragment()
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, step2Fragment)
.commit()
}
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
observeAddressSubmissionState()
@ -503,7 +508,9 @@ class RegisterStep3Fragment : Fragment() {
private fun showRegistrationSuccess() {
// Now we can show the success message for the overall registration process
Toast.makeText(requireContext(), "Registration completed successfully!", Toast.LENGTH_LONG).show()
Toast.makeText(requireContext(), "Berhasil mendaftarkan akun", Toast.LENGTH_LONG).show()
sessionManager.clearAll()
// Navigate to login screen
startActivity(Intent(requireContext(), LoginActivity::class.java))
@ -521,4 +528,5 @@ class RegisterStep3Fragment : Fragment() {
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
_binding = null
}
}

View File

@ -30,6 +30,8 @@ class CartActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager
private lateinit var storeAdapter: StoreAdapter
private var TAG = "Cart Activity"
private val viewModel: CartViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
@ -39,11 +41,14 @@ class CartActivity : AppCompatActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
if (!sessionManager.isLoggedIn()){
binding.emptyCart.text = "Silahkan masuk terlebih dahulu"
}
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -116,7 +121,7 @@ class CartActivity : AppCompatActivity() {
// Start checkout with the prepared items
startCheckoutWithWholesaleInfo(selectedItems)
} 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 {
@ -135,12 +140,44 @@ class CartActivity : AppCompatActivity() {
}
private fun startCheckoutWithWholesaleInfo(checkoutItems: List<CartItemCheckoutInfo>) {
// Extract cart item IDs and wholesale status
val cartItemIds = checkoutItems.map { it.cartItem.cartItemId }
val wholesaleArray = checkoutItems.map { it.isWholesale }.toBooleanArray()
val wholesalePriceMap = viewModel.cartItemWholesalePrice.value ?: emptyMap()
// Start checkout activity with the cart items and wholesale info
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray)
val updatedItems = checkoutItems.map { info ->
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() {
@ -162,7 +199,8 @@ class CartActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { errorMessage ->
errorMessage?.let {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
binding.emptyCart.visibility = View.VISIBLE
Log.e("CartActivity", "Error message: $it")
}
}
@ -215,6 +253,10 @@ class CartActivity : AppCompatActivity() {
storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap)
}
}
viewModel.productImages.observe(this) { productImages ->
storeAdapter.updateProductImages(productImages)
}
}
private fun showEmptyState(isEmpty: Boolean) {
@ -234,6 +276,4 @@ class CartActivity : AppCompatActivity() {
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.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.Product
import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import kotlinx.coroutines.launch
@ -52,6 +53,12 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
private val _hasConsistentWholesaleStatus = MutableLiveData<Boolean>(true)
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() {
_isLoading.value = true
_errorMessage.value = null
@ -62,6 +69,12 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
_cartItems.value = result.data
_isLoading.value = false
result.data.forEach { store ->
store.cartItems.forEach { item ->
loadProductImage(item.productId)
}
}
// After loading cart items, check wholesale status
checkWholesaleStatus()
}
@ -404,4 +417,29 @@ class CartViewModel(private val repository: OrderRepository) : ViewModel() {
_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.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.customer.cart.CartItemsItem
import com.alya.ecommerce_serang.data.api.response.customer.cart.DataItemCart
@ -30,6 +31,12 @@ class StoreAdapter(
private var activeStoreId: Int? = null
private var wholesaleStatusMap: Map<Int, Boolean> = 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 {
private const val VIEW_TYPE_STORE = 0
@ -135,7 +142,8 @@ class StoreAdapter(
wholesalePrice,
{ isChecked -> onItemCheckChanged(cartItem.cartItemId, store.storeId, isChecked) },
{ quantity -> onItemQuantityChanged(cartItem.cartItemId, quantity) },
{ onItemDeleted(cartItem.cartItemId) }
{ onItemDeleted(cartItem.cartItemId) },
productImages
)
}
}
@ -197,7 +205,8 @@ class StoreAdapter(
wholesalePrice: Double?,
onCheckedChange: (Boolean) -> Unit,
onQuantityChanged: (Int) -> Unit,
onDelete: () -> Unit
onDelete: () -> Unit,
productImages: Map<Int, String>
) {
// Set product name
tvProductName.text = cartItem.productName
@ -216,20 +225,6 @@ class StoreAdapter(
// Set quantity
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
cbItem.setOnCheckedChangeListener(null)
cbItem.isChecked = isSelected
@ -247,11 +242,16 @@ class StoreAdapter(
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)
.load("https://example.com/images/${cartItem.productId}.jpg") // Assume image URL based on product ID
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.into(ivProduct)
// Quantity control

View File

@ -124,7 +124,7 @@ class ChatActivity : AppCompatActivity() {
if (token.isEmpty()) {
// 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))
finish()
return
@ -506,7 +506,7 @@ class ChatActivity : AppCompatActivity() {
}
startActivity(intent)
} 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)
}
}
@ -622,7 +622,7 @@ class ChatActivity : AppCompatActivity() {
if (outputFile.exists() && outputFile.length() > 0) {
if (outputFile.length() > 5 * 1024 * 1024) {
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
}

View File

@ -5,7 +5,6 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -80,8 +79,10 @@ class ChatListFragment : Fragment() {
}
}
is Result.Error -> {
binding.tvEmptyChat.visibility = View.VISIBLE
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 -> {
binding.progressBarChat.visibility = View.VISIBLE

View File

@ -6,6 +6,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
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.viewmodel.HomeUiState
import com.alya.ecommerce_serang.utils.viewmodel.HomeViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
//@AndroidEntryPoint
@ -67,12 +69,10 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
setupRecyclerView()
observeData()
setupSearchView()
}
private fun setupRecyclerView() {
@ -140,24 +140,26 @@ class HomeFragment : Fragment() {
viewModel.uiState.collect { state ->
when (state) {
is HomeUiState.Loading -> {
binding.loading.root.isVisible = true
binding.loadingAll.root.visibility = View.VISIBLE
binding.error.root.isVisible = false
binding.home.isVisible = false
}
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.home.isVisible = true
val products = state.products
viewModel.loadStoresForProducts(products) // << add this here
productAdapter?.updateLimitedProducts(products)
}
is HomeUiState.Error -> {
binding.loading.root.isVisible = false
binding.loadingAll.root.visibility = View.GONE
binding.error.root.isVisible = true
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 {
viewModel.retry()
}
@ -166,7 +168,6 @@ class HomeFragment : Fragment() {
}
}
}
viewLifecycleOwner.lifecycleScope.launch {

View File

@ -7,12 +7,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
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.retrofit.ApiConfig
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()) {
requestFocus()
post {

View File

@ -1,9 +1,11 @@
package com.alya.ecommerce_serang.ui.order
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
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.util.Locale
class CartCheckoutAdapter(private val checkoutData: CheckoutData) :
RecyclerView.Adapter<CartCheckoutAdapter.SellerViewHolder>() {
class CartCheckoutAdapter(
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 {
val binding = ItemOrderSellerBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return SellerViewHolder(binding)
}
override fun getItemCount(): Int = 1 // Only one seller
override fun onBindViewHolder(holder: SellerViewHolder, position: Int) {
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView with multiple items
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = MultiCartItemsAdapter(checkoutData.cartItems)
class SellerViewHolder(val binding: ItemOrderSellerBinding) : RecyclerView.ViewHolder(binding.root) {
val childAdapter = MultiCartItemsAdapter(emptyList(), emptyMap())
init {
binding.rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(binding.root.context)
adapter = childAdapter
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>) :
RecyclerView.Adapter<MultiCartItemsAdapter.CartItemViewHolder>() {
class MultiCartItemsAdapter(
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)
@ -56,22 +78,55 @@ class MultiCartItemsAdapter(private val cartItems: List<CartItemsItem>) :
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) {
val item = cartItems[position]
Log.d("MultiCartItemsAdapter", "onBindViewHolder - position: $position, productId: ${item.productId}")
Log.d("MultiCartItemsAdapter", "Available images: $productImages")
with(holder.binding) {
// Set cart item details
tvProductName.text = item.productName
tvProductQuantity.text = "${item.quantity} buah"
tvProductPrice.text = formatCurrency(item.price.toDouble())
// Load placeholder image
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)
.load(R.drawable.placeholder_image)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image) // Add error handling
.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 {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount).replace(",00", "")

View File

@ -26,6 +26,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat
import java.util.Locale
@ -35,6 +36,8 @@ class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding
private lateinit var sessionManager: SessionManager
private var paymentAdapter: PaymentMethodAdapter? = null
private var cartCheckoutAdapter: CartCheckoutAdapter? = null
private var checkoutSellerAdapter: CheckoutSellerAdapter? = null
private var paymentMethodsLoaded = false
private val viewModel: CheckoutViewModel by viewModels {
@ -96,18 +99,40 @@ class CheckoutActivity : AppCompatActivity() {
// Process Cart checkout flow
val cartItemIds = intent.getIntArrayExtra(EXTRA_CART_ITEM_IDS)?.toList() ?: emptyList()
val isWholesaleArray = intent.getBooleanArrayExtra(EXTRA_CART_ITEM_WHOLESALE)
val wholesalePricesArray = intent.getIntArrayExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES)
if (cartItemIds.isNotEmpty()) {
// Create a map of cart item IDs to wholesale status if available
val wholesaleMap = if (isWholesaleArray != null && isWholesaleArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id -> id to isWholesaleArray[index] }.toMap()
// Build map of cartItemId -> isWholesale
val isWholesaleMap = if (isWholesaleArray != null && isWholesaleArray.size == cartItemIds.size) {
cartItemIds.mapIndexed { index, id ->
id to isWholesaleArray[index]
}.toMap()
} else {
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 {
Toast.makeText(this, "Error: No cart items specified", Toast.LENGTH_SHORT).show()
Toast.makeText(this, "Tidak ada item keranjang", Toast.LENGTH_SHORT).show()
finish()
}
}
@ -132,8 +157,18 @@ class CheckoutActivity : AppCompatActivity() {
// Observe address details
viewModel.addressDetails.observe(this) { address ->
binding.tvPlacesAddress.text = address?.recipient
binding.tvAddress.text = "${address?.street}, ${address?.subdistrict}"
if (address != null) {
// 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 ->
@ -150,9 +185,7 @@ class CheckoutActivity : AppCompatActivity() {
// Update the adapter ONLY if it exists
paymentAdapter?.let { adapter ->
// This line was causing issues - using setSelectedPayment instead of setSelectedPaymentName
adapter.setSelectedPaymentId(selectedPayment.id)
Log.d("CheckoutActivity", "Updated adapter with selected payment: ${selectedPayment.id}")
}
}
@ -161,33 +194,47 @@ class CheckoutActivity : AppCompatActivity() {
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
}
// Observe error messages
viewModel.errorMessage.observe(this) { message ->
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
viewModel.orderCreated.observe(this) { 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)
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>) {
if (paymentMethods.isEmpty()) {
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
}
binding.containerEmptyPayment.visibility = View.GONE
binding.rvPaymentInfo.visibility = View.VISIBLE
// Debug logging
Log.d("CheckoutActivity", "Setting up payment methods: ${paymentMethods.size} methods available")
@ -233,17 +280,47 @@ class CheckoutActivity : AppCompatActivity() {
}
private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData)
if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
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 {
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 {
layoutManager = LinearLayoutManager(this@CheckoutActivity)
this.adapter = adapter
isNestedScrollingEnabled = false
}
binding.containerEmptyProducts.visibility = View.GONE
binding.rvProductItems.visibility = View.VISIBLE
}
private fun updateOrderSummary() {
@ -268,7 +345,8 @@ class CheckoutActivity : AppCompatActivity() {
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
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"
@ -276,6 +354,8 @@ class CheckoutActivity : AppCompatActivity() {
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
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
binding.layoutShippingMethod.setOnClickListener {
binding.tvShippingOption.setOnClickListener {
val addressId = viewModel.addressDetails.value?.id ?: 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
}
@ -322,7 +402,16 @@ class CheckoutActivity : AppCompatActivity() {
// Create order button
binding.btnPay.setOnClickListener {
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)
// 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_ISWHOLESALE = "ISWHOLESALE"
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
@ -449,13 +539,17 @@ class CheckoutActivity : AppCompatActivity() {
fun startForCart(
context: Context,
cartItemIds: List<Int>,
isWholesaleArray: BooleanArray? = null
isWholesaleArray: BooleanArray? = null,
wholesalePrices: IntArray? = null
) {
val intent = Intent(context, CheckoutActivity::class.java).apply {
putExtra(EXTRA_CART_ITEM_IDS, cartItemIds.toIntArray())
if (isWholesaleArray != null) {
putExtra(EXTRA_CART_ITEM_WHOLESALE, isWholesaleArray)
}
if (wholesalePrices != null) {
putExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES, wholesalePrices)
}
}
context.startActivity(intent)
}

View File

@ -11,31 +11,53 @@ import com.alya.ecommerce_serang.databinding.ItemOrderSellerBinding
class CheckoutSellerAdapter(private val checkoutData: CheckoutData) :
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)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SellerViewHolder {
val binding = ItemOrderSellerBinding.inflate(
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) {
currentViewHolder = holder
with(holder.binding) {
// Set seller name
tvStoreName.text = checkoutData.sellerName
// Set up products RecyclerView
rvSellerOrderProduct.apply {
layoutManager = LinearLayoutManager(context)
adapter = if (checkoutData.isBuyNow) {
// Single product for Buy Now
SingleProductAdapter(checkoutData)
} else {
// Single cart item
SingleCartItemAdapter(checkoutData.cartItems.first())
SingleCartItemAdapter(checkoutData.cartItems.first()).also { adapter ->
// Apply existing images if available
if (productImages.isNotEmpty()) {
adapter.updateProductImages(productImages)
}
}
}
isNestedScrollingEnabled = false
}

View File

@ -40,7 +40,10 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCreated = MutableLiveData<Boolean>()
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
fun initializeBuyNow(
@ -93,30 +96,46 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
// 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 {
_isLoading.value = true
try {
// Get cart data
val cartResult = repository.getCart()
if (cartResult is Result.Success) {
// Find matching cart items
val matchingItems = mutableListOf<CartItemsItem>()
var storeData: DataItemCart? = null
for (store in cartResult.data) {
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
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
break
}
}
if (matchingItems.isNotEmpty() && storeData != null) {
// Create initial OrderRequest object
val orderRequest = OrderRequest(
addressId = 0,
paymentMethodId = 0,
@ -126,21 +145,32 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
isNego = false,
cartItemId = cartItemIds,
shipEtd = "",
// Add a list tracking which items are wholesale
isReseller = isWholesaleMap.any { it.value } // Set true if any item is wholesale
isReseller = isWholesaleMap.any { it.value }
)
// Create checkout data
Log.d(TAG, "Cek is reseller: ${orderRequest.isReseller}")
_checkoutData.value = CheckoutData(
orderRequest = orderRequest,
productName = matchingItems.first().productName,
sellerName = storeData.storeName,
sellerId = storeData.storeId,
isBuyNow = false,
cartItems = matchingItems,
cartItemWholesaleMap = isWholesaleMap // Store the wholesale map
cartItems = matchingItems, // These now have updated wholesale prices
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()
calculateTotal()
} else {
@ -151,12 +181,15 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
} catch (e: Exception) {
_errorMessage.value = "Error: ${e.message}"
Log.e(TAG, "Error in initializeFromCart", e)
} finally {
_isLoading.value = false
}
}
}
fun getPaymentMethods() {
viewModelScope.launch {
try {
@ -356,6 +389,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} else {
// For Cart checkout, use the standard order endpoint
val cartRequest = data.orderRequest as OrderRequest
Log.d(TAG, "data: ${cartRequest.cartItemId}")
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
private fun getShippingPrice(): Double {
val data = _checkoutData.value ?: return 0.0
@ -405,9 +464,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
companion object {
private const val TAG = "CheckoutViewModel"
}
}

View File

@ -67,7 +67,7 @@ class ShippingActivity : AppCompatActivity() {
// Validate required information
if (addressId <= 0 || productId <= 0) {
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()
return
}

View File

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

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CheckoutData
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
@ -36,9 +37,16 @@ class SingleProductAdapter(private val checkoutData: CheckoutData) :
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
Glide.with(ivProduct.context)
.load(checkoutData.productImageUrl)
.load(fullImageUrl)
.apply(
RequestOptions()
.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.WindowInsetsCompat
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.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.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.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var profileUser: Int = 1
private lateinit var locationManager: LocationManager
private var profileUserId: Int? = null
private var isRequestingLocation = false
@ -46,6 +49,8 @@ class AddAddressActivity : AppCompatActivity() {
private var longitude: Double? = 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 val viewModel: AddAddressViewModel by viewModels {
SavedStateViewModelFactory(this) { savedStateHandle ->
@ -61,6 +66,12 @@ class AddAddressActivity : AppCompatActivity() {
binding = ActivityAddAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
applyLiveCounter(
binding.etDetailAlamat,
binding.tvCountDetail,
binding.tvCountDetailMax
)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
@ -80,11 +91,15 @@ class AddAddressActivity : AppCompatActivity() {
)
windowInsets
}
viewModel.loadUserProfile()
// Get user profile from session manager
// profileUser =UserProfile.
viewModel.userProfile.observe(this){ user ->
user?.let { updateProfile(it) }
viewModel.userProfile.observe(this) { user ->
if (user != null) {
profileUserId = user.userId
Log.d(TAG, "Fetched userId = $profileUserId") // ✅ debug log
} else {
Log.e(TAG, "Error get profile")
}
}
setupToolbar()
@ -94,16 +109,10 @@ class AddAddressActivity : AppCompatActivity() {
setupButtonListeners()
setupObservers()
// Force trigger province loading to ensure it happens
viewModel.getProvinces()
}
private fun updateProfile(userProfile: UserProfile){
profileUser = userProfile.userId
}
// UI setup methods
private fun setupToolbar() {
binding.toolbar.setNavigationOnClickListener {
@ -116,6 +125,8 @@ class AddAddressActivity : AppCompatActivity() {
// Set adapters
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
binding.autoCompleteDesa.setAdapter(villageAdapter)
// Make dropdown appear on click (not just when typing)
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
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
val provinceId = provinceAdapter.getProvinceId(position)
@ -152,9 +183,41 @@ class AddAddressActivity : AppCompatActivity() {
cityId?.let { id ->
Log.d(TAG, "Setting selectedCityId=$id")
viewModel.getSubdistrict(cityId)
viewModel.selectedCityId = id
binding.autoCompleteKecamatan.text.clear()
} ?: 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() {
@ -178,6 +241,16 @@ class AddAddressActivity : AppCompatActivity() {
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
viewModel.addressSubmissionState.observe(this) { state ->
Log.d(TAG, "Received addressSubmissionState update: $state")
@ -202,7 +275,7 @@ class AddAddressActivity : AppCompatActivity() {
}
is ViewState.Error -> {
// Hide loading indicator
showError("Failed to load provinces: ${state.message}")
// showError("Failed to load provinces: ${state.message}")
Log.e("AddAddressActivity", "Province error: ${state.message}")
}
}
@ -221,12 +294,50 @@ class AddAddressActivity : AppCompatActivity() {
}
is ViewState.Error -> {
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}")
}
}
}
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>) {
when (state) {
is ViewState.Loading -> {
@ -276,23 +387,20 @@ class AddAddressActivity : AppCompatActivity() {
}
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 recipient = binding.etNamaPenerima.text.toString().trim()
val phone = binding.etNomorHp.text.toString().trim()
val userId = try {
profileUser
} catch (e: Exception) {
Log.w(TAG, "Error getting userId, using default", e)
1 // Default userId for testing
}
val userId = profileUserId
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
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, " +
"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")
// Validate required fields
@ -333,7 +441,7 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields
val request = CreateAddressRequest(
userId = userId,
userId = userId!!,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
@ -341,7 +449,7 @@ class AddAddressActivity : AppCompatActivity() {
cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
idVillage = "", // Or provide a real ID if needed
idVillage = villageId, // Or provide a real ID if needed
detailAddress = street,
isStoreLocation = false,
recipient = recipient,
@ -389,8 +497,8 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi tidak tersedia"
isRequestingLocation = false
Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
showEnableLocationDialog()
// Toast.makeText(this, "Provider lokasi tidak tersedia", Toast.LENGTH_SHORT).show()
// showEnableLocationDialog()
return
}
@ -417,7 +525,7 @@ class AddAddressActivity : AppCompatActivity() {
latitude = -6.200000
longitude = 106.816666
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
@ -431,7 +539,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${lastLocation.latitude}, ${lastLocation.longitude}"
isRequestingLocation = false
Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
// Toast.makeText(this, "Lokasi terdeteksi", Toast.LENGTH_SHORT).show()
return
} else {
Log.d(TAG, "No last known location, requesting updates")
@ -449,7 +557,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Lokasi terdeteksi: ${location.latitude}, ${location.longitude}"
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
try {
@ -472,7 +580,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Provider lokasi dimatikan"
isRequestingLocation = false
Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
// Toast.makeText(this@AddAddressActivity, "Provider $provider dimatikan", Toast.LENGTH_SHORT).show()
}
}
@ -491,7 +599,7 @@ class AddAddressActivity : AppCompatActivity() {
binding.locationProgressBar.visibility = View.GONE
binding.tvLocationStatus.text = "Error: ${e.message}"
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
latitude = -6.200000
@ -519,12 +627,12 @@ class AddAddressActivity : AppCompatActivity() {
// Add button to reload location (add this button to your layout)
binding.btnReloadLocation.setOnClickListener {
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()
}
}
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.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.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
@ -31,6 +34,18 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
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
var selectedProvinceId: Int?
get() = savedStateHandle.get<Int>("selectedProvinceId")
@ -40,6 +55,10 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
get() = savedStateHandle.get<String>("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value }
var selectedSubdistrict: String? = null
var selectedSubdistrictId: String? = null
var selectedVillages: String? = null
init {
// Load provinces on initialization
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) {
selectedProvinceId = id
}
fun updateSelectedCityId(id: String) {
selectedCityId = id
fun detailAddress(addressId: Int){
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 {
when (val result = userRepo.fetchUserProfile()){
is Result.Success -> _userProfile.postValue(result.data)
is Result.Error -> _errorMessageUser.postValue(result.exception.message ?: "Unknown Error")
is Result.Loading -> null
try {
val response = repository.updateAddress(oldAddress.id, params)
_editAddress.value = response.isSuccessful
} 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
}
viewModel.fetchAddresses()
setupToolbar()
setupRecyclerView()
setupObservers()
viewModel.fetchAddresses()
}
@ -74,13 +74,17 @@ class AddressActivity : AppCompatActivity() {
}
private fun setupRecyclerView() {
adapter = AddressAdapter { address ->
// Select the address in the ViewModel
viewModel.selectAddress(address.id)
// Return immediately with the selected address
returnResultAndFinish(address.id)
}
adapter = AddressAdapter(
onAddressClick = { address ->
viewModel.selectAddress(address.id)
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 {
layoutManager = LinearLayoutManager(this@AddressActivity)
@ -122,6 +126,11 @@ class AddressActivity : AppCompatActivity() {
finish()
}
override fun onResume() {
super.onResume()
viewModel.fetchAddresses()
}
companion object {
const val EXTRA_ADDRESS_ID = "extra_address_id"
}

View File

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

View File

@ -1,21 +1,458 @@
package com.alya.ecommerce_serang.ui.order.address
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import 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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_edit_address)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
private lateinit var binding: ActivityEditAddressBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var latitude: Double? = null
private var longitude: Double? = null
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

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log
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.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
@ -32,6 +33,10 @@ class ProvinceAdapter(
fun getProvinceId(position: Int): Int? {
return provinces.getOrNull(position)?.provinceId?.toIntOrNull()
}
fun getProvinceName(position: Int): String? {
return provinces.getOrNull(position)?.province?.toString()
}
}
class CityAdapter(
@ -53,6 +58,10 @@ class CityAdapter(
fun getCityId(position: Int): String? {
return cities.getOrNull(position)?.cityId?.toString()
}
fun getCityName(position: Int): String? {
return cities.getOrNull(position)?.cityName?.toString()
}
}
class SubdsitrictAdapter(
@ -99,6 +108,10 @@ class VillagesAdapter(
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
}
@ -122,23 +135,8 @@ class BankAdapter(
}
private fun loadHardcodedData() {
val defaultBanks = listOf(
BankItem("Bank Mandiri", "008", "PT Bank Mandiri (Persero) Tbk"),
BankItem("Bank BRI", "002", "PT Bank Rakyat Indonesia (Persero) Tbk"),
BankItem("Bank BCA", "014", "PT Bank Central Asia Tbk"),
BankItem("Bank BNI", "009", "PT Bank Negara Indonesia (Persero) Tbk"),
BankItem("Bank BTN", "200", "PT Bank Tabungan Negara (Persero) Tbk"),
BankItem("Bank CIMB Niaga", "022", "PT Bank CIMB Niaga Tbk"),
BankItem("Bank Danamon", "011", "PT Bank Danamon Indonesia Tbk"),
BankItem("Bank Permata", "013", "PT Bank Permata Tbk"),
BankItem("Bank OCBC NISP", "028", "PT Bank OCBC NISP Tbk"),
BankItem("Bank Maybank", "016", "PT Bank Maybank Indonesia Tbk"),
BankItem("Bank Panin", "019", "PT Bank Panin Dubai Syariah Tbk"),
BankItem("Bank UOB", "023", "PT Bank UOB Indonesia"),
BankItem("Bank Mega", "426", "PT Bank Mega Tbk"),
BankItem("Bank Bukopin", "441", "PT Bank Bukopin Tbk"),
BankItem("Bank BJB", "110", "PT Bank Pembangunan Daerah Jawa Barat dan Banten Tbk")
)
val bankNames = context.resources.getStringArray(R.array.bank_names)
val defaultBanks = bankNames.map { BankItem(bankName = it) }
updateData(defaultBanks)
}

View File

@ -33,6 +33,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@ -61,8 +62,8 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
private val paymentMethods = arrayOf(
"Pilih Metode Pembayaran",
"Transfer Bank",
"QRIS",
)
// private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
@ -122,7 +123,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
} catch (e: Exception) {
Log.e(TAG, "ERROR in AddEvidencePaymentActivity onCreate: ${e.message}", e)
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
}
}
@ -167,6 +167,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// Submit button
binding.btnSubmit.setOnClickListener {
validateAndUpload()
Log.d(TAG, "AddEvidencePaymentActivity onCreate completed")
}
@ -221,8 +222,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
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 {
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
}
}
@ -288,7 +287,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
}
} catch (e: Exception) {
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()
}
}
@ -314,25 +313,35 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
return
}
if (binding.spinnerPaymentMethod.selectedItemPosition == 0) {
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
return
}
//in case applied metode pembayaran yang lain
// if (binding.spinnerPaymentMethod.selectedItemPosition == 0) {
// Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
// return
// }
binding.etAccountNumber.visibility = View.GONE
//in case applied nomor rekening
// if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
// Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
// return
// }
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
return
}
// if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
// Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
// return
// }
// 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() {
@ -367,7 +376,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
viewModel.uploadPaymentProof(request)
} catch (e: Exception) {
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()
}
}
}
@ -444,9 +453,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
).show()
}
companion object {
private const val PERMISSION_REQUEST_CODE = 100
private const val TAG = "AddEvidenceActivity"

View File

@ -160,7 +160,8 @@ class PaymentActivity : AppCompatActivity() {
viewModel.error.observe(this) { error ->
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()
}
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null)

View File

@ -27,7 +27,6 @@ class HistoryActivity : AppCompatActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)

View File

@ -37,9 +37,6 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
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>>()
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
@ -113,83 +110,6 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
ViewState.Loading // ② initial value, still fine
)
// fun getOrderList(status: String) {
// _orders.value = ViewState.Loading
// viewModelScope.launch {
// try {
// if (status == "all") {
// // Get all orders by combining all statuses
// getAllOrdersCombined()
// } else {
// // Get orders for specific status
// when (val result = repository.getOrderList(status)) {
// is Result.Success -> {
// _orders.value = ViewState.Success(result.data.orders)
// Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items")
// }
// is Result.Error -> {
// _orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
// Log.e(TAG, "Error loading orders", result.exception)
// }
// is Result.Loading -> {
// // Keep loading state
// }
// }
// }
// } catch (e: Exception) {
// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
// Log.e(TAG, "Exception in getOrderList", e)
// }
// }
// }
// private suspend fun getAllOrdersCombined() {
// try {
// val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
// val allOrders = mutableListOf<OrdersItem>()
//
// // Use coroutineScope to allow launching async blocks
// coroutineScope {
// val deferreds = allStatuses.map { status ->
// async {
// when (val result = repository.getOrderList(status)) {
// is Result.Success -> {
// // Tag each order with the status it was fetched from
// result.data.orders.onEach { it.displayStatus = status }
// }
// is Result.Error -> {
// Log.e(TAG, "Error loading orders for status $status", result.exception)
// emptyList<OrdersItem>()
// }
// is Result.Loading -> emptyList<OrdersItem>()
// }
// }
// }
//
// // Await all results and combine
// deferreds.awaitAll().forEach { orders ->
// allOrders.addAll(orders)
// }
// }
//
// // Sort orders
// val sortedOrders = allOrders.sortedByDescending { order ->
// try {
// SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
// } catch (e: Exception) {
// null
// }
// }
//
// _orders.value = ViewState.Success(sortedOrders)
// Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
//
// } catch (e: Exception) {
// _orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
// Log.e(TAG, "Exception in getAllOrdersCombined", e)
// }
// }
private suspend fun getAllOrdersCombined(): ViewState<List<OrdersItem>> = try {
val statuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
@ -292,11 +212,6 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
// fun refreshOrders(status: String = "all") {
// Log.d(TAG, "Refreshing orders with status: $status")
// // Don't set Loading here if you want to show current data while refreshing
// getOrderList(status)
// }
fun updateStatus(status: String, forceRefresh: Boolean = false) {
Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
@ -316,19 +231,21 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
}
}
private suspend fun refresh(status: String) {
fun refresh(status: String) {
Log.d(TAG, "⏳ refresh(\"$status\") started")
try {
if (status == "all") {
Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
getAllOrdersCombined() // network → cache
} else {
Log.d(TAG, "🌐 repository.getOrderList(\"$status\")")
repository.getOrderList(status) // network → cache
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
}
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)
}

View File

@ -28,6 +28,7 @@ import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
@ -41,7 +42,8 @@ import java.util.TimeZone
class OrderHistoryAdapter(
private val onOrderClickListener: (OrdersItem) -> Unit,
private val viewModel: HistoryViewModel,
private val callbacks: OrderActionCallbacks
private val callbacks: OrderActionCallbacks,
private val listener: OnDialogActionListener
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
interface OrderActionCallbacks {
@ -200,10 +202,6 @@ class OrderHistoryAdapter(
// viewModel.refreshOrders()
}
}
// deadlineDate.apply {
// visibility = View.VISIBLE
// text = formatDatePay(order.updatedAt)
// }
}
"processed" -> {
// Untuk status processed, tampilkan "Hubungi Penjual"
@ -215,15 +213,6 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_processed)
}
// gabisa complaint
// btnLeft.apply {
// visibility = View.VISIBLE
// text = itemView.context.getString(R.string.canceled_order_btn)
// setOnClickListener {
// showCancelOrderDialog(order.orderId.toString())
// viewModel.refreshOrders()
// }
// }
}
"shipped" -> {
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
@ -250,9 +239,18 @@ class OrderHistoryAdapter(
callbacks.onShowLoading(true)
// Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed")
PopUpDialog.showConfirmDialog(
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()
}
}
@ -517,14 +515,14 @@ class OrderHistoryAdapter(
} else {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Terjadi kendala di batalkan pesanan", Toast.LENGTH_SHORT).show()
return
}
}
else -> {
// Log error and show a Toast instead if we can't get a FragmentManager
Log.e("OrderHistoryAdapter", "Cannot show bottom sheet: Context is not a FragmentActivity")
Toast.makeText(context, "Cannot show cancel order dialog", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Terjadi kendala di batalkan pesanan", Toast.LENGTH_SHORT).show()
return
}
}
@ -533,9 +531,19 @@ class OrderHistoryAdapter(
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Show a success message
Toast.makeText(context, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
PopUpDialog.showConfirmDialog(
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()
}
)
}
)
@ -547,18 +555,10 @@ class OrderHistoryAdapter(
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)
// Create loading dialog
// val loadingDialog = Dialog(itemView.context).apply {
// requestWindowFeature(Window.FEATURE_NO_TITLE)
// setContentView(R.layout.dialog_loading)
// window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// setCancelable(false)
// }
// loadingDialog.show()
viewModel.error.observe(itemView.findViewTreeLifecycleOwner()!!) { errorMsg ->
if (!errorMsg.isNullOrEmpty()) {
Toast.makeText(itemView.context, errorMsg, Toast.LENGTH_SHORT).show()
Log.e("OrderHistoryAdapter", "Error $errorMsg")
Toast.makeText(itemView.context, "Terdapat kendala di tambah ulasan", Toast.LENGTH_SHORT).show()
}
}
@ -594,7 +594,7 @@ class OrderHistoryAdapter(
} else {
Toast.makeText(
itemView.context,
"No items to review",
"Tidak ada produk untuk direview",
Toast.LENGTH_SHORT
).show()
}
@ -624,3 +624,7 @@ class OrderHistoryAdapter(
}
}
}
interface OnDialogActionListener {
fun onDialogConfirmed()
}

View File

@ -44,8 +44,6 @@ class OrderHistoryFragment : Fragment() {
sessionManager = SessionManager(requireContext())
setupViewPager()
}
private fun setupViewPager() {
@ -67,17 +65,25 @@ class OrderHistoryFragment : Fragment() {
}
}.attach()
statusPage()
}
private fun statusPage(){
binding.viewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val status = viewPagerAdapter.orderStatuses[position]
/* setStatus() is the API we added earlier; TRUE → always requery */
historyVm.updateStatus(status, forceRefresh = true)
}
}
)
}
override fun onResume() {
super.onResume()
statusPage()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.order.history
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -23,7 +24,7 @@ import com.alya.ecommerce_serang.ui.order.history.detailorder.DetailOrderStatusA
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks, OnDialogActionListener {
private var _binding: FragmentOrderListBinding? = null
private val binding get() = _binding!!
@ -84,8 +85,6 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
setupRecyclerView()
observeOrderList()
observeViewModel()
// observeOrderCompletionStatus()
// loadOrders()
}
private fun setupRecyclerView() {
@ -94,7 +93,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
navigateToOrderDetail(order)
},
viewModel = viewModel,
callbacks = this // Pass this fragment as callback
callbacks = this,
listener = this// Pass this fragment as callback
)
orderAdapter.setFragmentStatus(status)
@ -106,31 +106,6 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
}
private fun observeOrderList() {
// Now we only need to observe one LiveData for all cases
// viewModel.orders.observe(viewLifecycleOwner) { result ->
// when (result) {
// is ViewState.Success -> {
// binding.progressBar.visibility = View.GONE
//
// if (result.data.isNullOrEmpty()) {
// binding.tvEmptyState.visibility = View.VISIBLE
// binding.rvOrders.visibility = View.GONE
// } else {
// binding.tvEmptyState.visibility = View.GONE
// binding.rvOrders.visibility = View.VISIBLE
// orderAdapter.submitList(result.data)
// }
// }
// is ViewState.Error -> {
// binding.progressBar.visibility = View.GONE
// binding.tvEmptyState.visibility = View.VISIBLE
// Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
// }
// is ViewState.Loading -> {
// binding.progressBar.visibility = View.VISIBLE
// }
// }
// }
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.orders.collect { state ->
when (state) {
@ -141,7 +116,7 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
binding.progressBar.isVisible = false
binding.tvEmptyState.isVisible = true
binding.rvOrders.isVisible = false
Toast.makeText(requireContext(), state.message, Toast.LENGTH_SHORT).show()
Log.e("OrderListFragment", "Error in order list: ${state.message}")
}
is ViewState.Success -> {
binding.progressBar.isVisible = false
@ -157,47 +132,19 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
}
private fun observeViewModel() {
// Observe order completion
// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
// when (result) {
// is Result.Success -> {
// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
//// loadOrders() // Refresh here
// }
// is Result.Error -> {
// Toast.makeText(requireContext(), "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// }
// is Result.Loading -> {
// // Show loading if needed
// }
// }
// }
//
// // Observe cancel order status
// viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
// when (result) {
// is Result.Success -> {
// Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
// loadOrders() // Refresh here
// }
// is Result.Error -> {
// Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// }
// is Result.Loading -> {
// // Show loading if needed
// }
// }
// }
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
when (result) {
is Result.Success -> {
Toast.makeText(requireContext(),
"Order completed!", Toast.LENGTH_SHORT).show()
"Pesanan Selesai", Toast.LENGTH_SHORT).show()
Log.d("OrderListFragment", "Order selesai")
viewModel.updateStatus(status, forceRefresh = true)
}
is Result.Error ->
Toast.makeText(requireContext(),
"Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// Toast.makeText(requireContext(),
// "Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
Log.e("OrderListFragment", "Failed: ${result.exception.message}")
else -> { /* Loading → no UI change */ }
}
}
@ -206,31 +153,19 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
when (result) {
is Result.Success -> {
Toast.makeText(requireContext(),
"Order cancelled!", Toast.LENGTH_SHORT).show()
"Pesanan Dibatalkan", Toast.LENGTH_SHORT).show()
Log.d("OrderListFragment", "Order dibatalkan")
viewModel.updateStatus(status, forceRefresh = true)
}
is Result.Error ->
Toast.makeText(requireContext(),
"Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
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) {
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
putExtra("ORDER_ID", order.orderId)
@ -239,11 +174,9 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
detailOrderLauncher.launch(intent)
}
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
Log.d("OrderListFragment", "Order cancel success: $message")
// loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true)
@ -254,11 +187,13 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
if (success) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), "Pesanan selesai", Toast.LENGTH_SHORT).show()
Log.d("OrderListFragment", "Pesanan selesai: $message")
// loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true)
} 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()
}
}
@ -271,20 +206,18 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
_binding = null
}
// private fun observeOrderCompletionStatus() {
// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
// when (result) {
// is Result.Loading -> {
// // Handle loading state if needed
// }
// is Result.Success -> {
// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
//// loadOrders()
// }
// is Result.Error -> {
// Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
// }
// }
// }
// }
override fun onResume() {
super.onResume()
observeOrderList()
}
override fun onDialogConfirmed() {
viewModel.refresh(status)
// Option 1: refresh seluruh fragment
requireActivity().supportFragmentManager.beginTransaction()
.detach(this)
.attach(this)
.commit()
}
}

View File

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

View File

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

View File

@ -90,7 +90,7 @@ class CreateReviewActivity : AppCompatActivity() {
)
})
} 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()
}
} else {
@ -110,7 +110,7 @@ class CreateReviewActivity : AppCompatActivity() {
)
)
} 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()
}
}

View File

@ -1,18 +1,26 @@
package com.alya.ecommerce_serang.ui.product
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
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.SessionManager
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 java.text.NumberFormat
import java.util.Locale
@ -51,6 +62,8 @@ class DetailProductActivity : AppCompatActivity() {
private var isWholesaleSelected: Boolean = false
private var minOrder: Int = 0
private var TAG = "DetailProductActivity"
private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
@ -112,13 +125,17 @@ class DetailProductActivity : AppCompatActivity() {
when (result) {
is Result.Success -> {
updateStoreInfo(result.data)
binding.progressBarDetailStore.visibility = View.GONE
}
is Result.Error -> {
// Show error message, maybe a Toast or Snackbar
Toast.makeText(this, "Failed to load store: ${result.exception.message}", Toast.LENGTH_SHORT).show()
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 -> {
// Show loading indicator if needed
binding.progressBarDetailStore.visibility = View.VISIBLE
}
}
}
@ -160,6 +177,10 @@ class DetailProductActivity : AppCompatActivity() {
val products = viewModel.otherProducts.value.orEmpty()
if (products.isNotEmpty()) {
updateOtherProducts(products, storeMap)
} else {
binding.emptyOtherProducts.visibility = View.VISIBLE
binding.recyclerViewOtherProducts.visibility = View.GONE
binding.tvViewAllProducts.visibility = View.GONE
}
}
}
@ -190,12 +211,14 @@ class DetailProductActivity : AppCompatActivity() {
private fun updateOtherProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) {
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
} else {
Log.d("DetailProductActivity", "Displaying product list in RecyclerView")
binding.recyclerViewOtherProducts.visibility = View.VISIBLE
binding.tvViewAllProducts.visibility = View.VISIBLE
binding.emptyOtherProducts.visibility = View.GONE
productAdapter = OtherProductAdapter(products, onClick = { product ->
handleProductClick(product)
@ -282,6 +305,16 @@ class DetailProductActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image)
.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()
@ -316,11 +349,13 @@ class DetailProductActivity : AppCompatActivity() {
val limitedReviewList = if (reviewList.isNotEmpty()) listOf(reviewList.first()) else emptyList()
if (reviewList.isEmpty()) {
binding.recyclerViewReviews.visibility = View.GONE
binding.emptyReview.visibility = View.VISIBLE
binding.tvViewAllReviews.visibility = View.GONE
// binding.tvNoReviews.visibility = View.VISIBLE
} else {
binding.recyclerViewReviews.visibility = View.VISIBLE
binding.tvViewAllReviews.visibility = View.VISIBLE
binding.emptyReview.visibility = View.GONE
}
// binding.tvNoReviews.visibility = View.GONE
reviewsAdapter = ReviewsAdapter(
@ -519,7 +554,64 @@ class DetailProductActivity : AppCompatActivity() {
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 {

View File

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

View File

@ -3,7 +3,7 @@ package com.alya.ecommerce_serang.ui.product.category
import android.view.LayoutInflater
import android.view.ViewGroup
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.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.databinding.ItemProductGridBinding
@ -46,8 +46,15 @@ class ProductsCategoryAdapter(
val priceValue = product.price.toDoubleOrNull() ?: 0.0
tvProductPrice.text = "Rp ${NumberFormat.getNumberInstance(Locale("id", "ID")).format(priceValue.toInt())}"
// 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)
.load("${BuildConfig.BASE_URL}${product.image}")
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.placeholder_image)
.centerCrop()
@ -57,15 +64,6 @@ class ProductsCategoryAdapter(
root.setOnClickListener {
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
}
loadData()
setupUI()
setupObservers()
loadData()
}
private fun setupUI() {
@ -88,15 +87,18 @@ class StoreDetailActivity : AppCompatActivity() {
viewModel.storeDetail.observe(this) { result ->
when (result) {
is Result.Success -> {
binding.progressBarDetailProdItem.visibility = View.GONE
updateStoreInfo(result.data)
viewModel.loadOtherProducts(result.data.storeId)
}
is Result.Error -> {
// 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()
}
is Result.Loading -> {
// Show loading indicator if needed
binding.progressBarDetailProdItem.visibility = View.VISIBLE
}
}
}
@ -109,6 +111,9 @@ class StoreDetailActivity : AppCompatActivity() {
val products = viewModel.otherProducts.value.orEmpty()
if (products.isNotEmpty()) {
updateProducts(products, storeMap)
} else {
binding.progressBarDetailProdItem.visibility = View.VISIBLE
binding.rvProducts.visibility = View.GONE
}
}
}
@ -146,7 +151,7 @@ class StoreDetailActivity : AppCompatActivity() {
.into(binding.ivStoreImage)
val ratingStr = it.storeRating
val ratingValue = ratingStr?.toFloatOrNull()
val ratingValue = ratingStr?.toFloatOrNull() ?: 0f
if (ratingValue != null && ratingValue > 0f) {
binding.tvStoreRating.text = String.format("%.1f", ratingValue)
@ -161,10 +166,12 @@ class StoreDetailActivity : AppCompatActivity() {
private fun updateProducts(products: List<ProductsItem>, storeMap: Map<Int, StoreItem>) {
if (products.isEmpty()) {
binding.rvProducts.visibility = View.GONE
binding.progressBarDetailProdItem.visibility = View.VISIBLE
Log.d("StoreDetailActivity", "Product list is empty, hiding RecyclerView")
} else {
Log.d("StoreDetailActivity", "Displaying product list in RecyclerView")
binding.progressBarDetailProdItem.visibility = View.GONE
binding.rvProducts.visibility = View.VISIBLE
productAdapter = HorizontalProductAdapter(products, onClick = { product ->
handleProductClick(product)

View File

@ -45,11 +45,11 @@ class StoreDetailViewModel (private val repository: ProductRepository
} // Filter by storeId and exclude current product
_otherProducts.value = filteredProducts // Update LiveData
} else if (result is Result.Error) {
Log.e("ProductViewModel", "Error loading other products: ${result.exception.message}")
Log.e("StoreDetailViewModel", "Error loading other products: ${result.exception.message}")
_otherProducts.value = emptyList() // Set empty list on failure
}
} catch (e: Exception) {
Log.e("ProductViewModel", "Exception loading other products: ${e.message}")
Log.e("StoreDetailViewModel", "Exception loading other products: ${e.message}")
_otherProducts.value = emptyList()
}
}
@ -67,7 +67,7 @@ class StoreDetailViewModel (private val repository: ProductRepository
loadStoreDetail(storeId)
}
} 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}"
} finally {
_isLoading.value = false
@ -82,7 +82,7 @@ class StoreDetailViewModel (private val repository: ProductRepository
val result = repository.fetchStoreDetail(storeId)
_storeDetail.value = result
} 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)
}
}
@ -99,10 +99,10 @@ class StoreDetailViewModel (private val repository: ProductRepository
if (result is Result.Success) {
map[storeId] = result.data
} 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) {
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")
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
private fun setupClickListeners() {
@ -106,7 +108,8 @@ class DetailProfileActivity : AppCompatActivity() {
}
editProfileLauncher.launch(intent)
} ?: 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
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
@ -20,13 +19,14 @@ import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.auth.LoginActivity
import com.alya.ecommerce_serang.ui.profile.mystore.RegisterStoreActivity
import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.ui.order.history.HistoryActivity
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.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
@ -58,7 +58,6 @@ class ProfileFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
}
override fun onCreateView(
@ -72,26 +71,59 @@ class ProfileFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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.checkStoreUser()
observeUserProfile()
observeStoreStatus()
viewModel.loadUserProfile()
viewModel.checkStoreUser()
binding.cardBukaToko.setOnClickListener{
// if (hasStore == true) startActivity(Intent(requireContext(), MyStoreActivity::class.java))
// else startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
if (viewModel.checkStore.value == true) {
myStoreViewModel.loadMyStore()
myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { store ->
store?.let {
when (store.storeStatus) {
myStoreViewModel.myStoreProfile.observe(viewLifecycleOwner) { storeDataResponse ->
storeDataResponse?.let { storeResponse ->
val store = storeResponse.store
when (store.approvalStatus) {
"process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java))
"active" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
"inactive" -> startActivity(Intent(requireContext(), MyStoreActivity::class.java))
"suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java))
else -> startActivity(Intent(requireContext(), RegisterStoreActivity::class.java))
"rejected" -> startActivity(
Intent(requireContext(), RegisterStoreActivity::class.java)
.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()
@ -115,6 +147,11 @@ class ProfileFragment : Fragment() {
startActivity(intent)
}
binding.cardChangePass.setOnClickListener{
val intent = Intent(requireContext(), ChangePasswordActivity::class.java)
startActivity(intent)
}
binding.cardLogout.setOnClickListener{
logout()
}
@ -130,7 +167,8 @@ class ProfileFragment : Fragment() {
user?.let { updateUI(it) }
}
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")
}
}
@ -162,14 +200,16 @@ class ProfileFragment : Fragment() {
private fun logout(){
AlertDialog.Builder(requireContext())
.setTitle("Konfirmasi")
.setMessage("Apakah anda yakin ingin keluar?")
.setPositiveButton("Ya") { _, _ ->
PopUpDialog.showConfirmDialog(
context = requireContext(),
title = "Konfirmasi",
message = "Apakah anda yakin ingin keluar?",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
actionLogout()
}
.setNegativeButton("Tidak", null)
.show()
)
}
private fun actionLogout(){
@ -184,8 +224,11 @@ class ProfileFragment : Fragment() {
delay(500)
loadingDialog.dismiss()
sessionManager.clearAll()
viewModel.deleteFCM()
val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent)
requireActivity().finish()
} catch (e: Exception) {
Toast.makeText(
requireContext(),
@ -196,4 +239,11 @@ class ProfileFragment : Fragment() {
}
}
override fun onResume() {
super.onResume()
viewModel.loadUserProfile()
viewModel.checkStoreUser()
}
}

View File

@ -1,10 +1,12 @@
package com.alya.ecommerce_serang.ui.profile.editprofile
import android.Manifest
import android.app.Activity
import android.app.DatePickerDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
@ -22,16 +24,15 @@ import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityEditProfileCustBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide
import com.google.gson.Gson
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -39,9 +40,9 @@ import java.util.TimeZone
class EditProfileCustActivity : AppCompatActivity() {
private lateinit var binding: ActivityEditProfileCustBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private var selectedImageUri: Uri? = null
private var currentUser: UserProfile? = null
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
@ -52,7 +53,7 @@ class EditProfileCustActivity : AppCompatActivity() {
}
private val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
val data: Intent? = result.data
data?.data?.let {
selectedImageUri = it
@ -103,8 +104,8 @@ class EditProfileCustActivity : AppCompatActivity() {
}
userProfile?.let {
currentUser = it
populateFields(it)
setupClickListeners()
observeViewModel()
}
@ -116,7 +117,7 @@ class EditProfileCustActivity : AppCompatActivity() {
binding.etNumberPhoneUser.setText(profile.phone)
// Format birth date for display
profile.birthDate?.let {
profile.birthDate.let {
binding.etDateBirth.setText(formatDate(it))
}
@ -154,22 +155,19 @@ class EditProfileCustActivity : AppCompatActivity() {
}
binding.btnSave.setOnClickListener {
saveProfile()
if (hasChanged()) confirmUpdate() else finish()
}
}
private fun openImagePicker() {
// Check for permission first
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
REQUEST_STORAGE_PERMISSION
)
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(permission), REQUEST_STORAGE_PERMISSION)
} else {
launchImagePicker()
}
@ -214,25 +212,43 @@ class EditProfileCustActivity : AppCompatActivity() {
datePickerDialog.show()
}
private fun confirmUpdate() {
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah Anda yakin ingin menyimpan perubahan profil?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
saveProfile()
}
)
}
private fun hasChanged(): Boolean {
val name = binding.etNameUser.text.toString() != currentUser?.name
val username = binding.etUsername.text.toString() != currentUser?.username
val email = binding.etEmailUser.text.toString() != currentUser?.email
val phone = binding.etNumberPhoneUser.text.toString() != currentUser?.phone
val displayDate = binding.etDateBirth.text.toString() != currentUser?.birthDate.toString()
val imgProfile = selectedImageUri != null
return name || username || email || phone || displayDate || imgProfile
}
private fun saveProfile() {
val name = binding.etNameUser.text.toString()
val username = binding.etUsername.text.toString()
val email = binding.etEmailUser.text.toString()
val phone = binding.etNumberPhoneUser.text.toString()
val displayDate = binding.etDateBirth.text.toString()
val imgProfile = selectedImageUri
if (name.isEmpty() || username.isEmpty() || email.isEmpty() || phone.isEmpty() || displayDate.isEmpty()) {
Toast.makeText(this, "Semua field harus diisi", Toast.LENGTH_SHORT).show()
return
}
// Convert date to server format
val serverBirthDate = convertToServerDateFormat(displayDate)
Log.d(TAG, "Starting profile save with direct method")
Log.d(TAG, "Selected image URI: $selectedImageUri")
// Disable the button to prevent multiple clicks
binding.btnSave.isEnabled = false
// Call the repository method via ViewModel
@ -243,82 +259,10 @@ class EditProfileCustActivity : AppCompatActivity() {
phone = phone,
birthDate = serverBirthDate,
email = email,
imageUri = selectedImageUri
imageUri = imgProfile
)
}
private fun getRealPathFromURI(uri: Uri): String? {
Log.d(TAG, "Getting real path from URI: $uri")
// Handle different URI schemes
when {
// File URI
uri.scheme == "file" -> {
val path = uri.path
Log.d(TAG, "URI is file scheme, path: $path")
return path
}
// Content URI
uri.scheme == "content" -> {
try {
val projection = arrayOf(MediaStore.Images.Media.DATA)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val path = cursor.getString(columnIndex)
Log.d(TAG, "Found path from content URI: $path")
return path
} else {
Log.e(TAG, "Cursor is empty")
}
} ?: Log.e(TAG, "Cursor is null")
// If the above fails, try the documented API way
contentResolver.openInputStream(uri)?.use { inputStream ->
// Create a temp file
val fileName = getFileName(uri) ?: "temp_img_${System.currentTimeMillis()}.jpg"
val tempFile = File(cacheDir, fileName)
tempFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
Log.d(TAG, "Created temporary file: ${tempFile.absolutePath}")
return tempFile.absolutePath
}
} catch (e: Exception) {
Log.e(TAG, "Error getting real path: ${e.message}", e)
}
}
}
Log.e(TAG, "Could not get real path for URI: $uri")
return null
}
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
if (columnIndex >= 0) {
result = cursor.getString(columnIndex)
Log.d(TAG, "Found filename from content URI: $result")
}
}
}
}
if (result == null) {
result = uri.path
val cut = result?.lastIndexOf('/') ?: -1
if (cut != -1) {
result = result?.substring(cut + 1)
}
Log.d(TAG, "Extracted filename from path: $result")
}
return result
}
private fun formatDate(dateString: String?): String {
if (dateString.isNullOrEmpty()) return "N/A"

View File

@ -51,7 +51,6 @@ class MyStoreActivity : AppCompatActivity() {
enableEdgeToEdge()
binding.headerMyStore.headerTitle.text = "Toko Saya"
binding.headerMyStore.headerLeftIcon.setOnClickListener {
@ -59,12 +58,13 @@ class MyStoreActivity : AppCompatActivity() {
finish()
}
viewModel.myStoreProfile.observe(this){ user ->
user?.let { myStoreProfileOverview(it.store) }
}
viewModel.loadMyStore()
viewModel.loadMyStoreProducts()
viewModel.myStoreProfile.observe(this){ user ->
user?.let { myStoreProfileOverview(it) }
}
viewModel.fetchBalance()
viewModel.errorMessage.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
@ -72,7 +72,6 @@ class MyStoreActivity : AppCompatActivity() {
setUpClickListeners()
getCountOrder()
observeViewModel()
viewModel.fetchBalance()
fetchBalance()
}
@ -107,22 +106,23 @@ class MyStoreActivity : AppCompatActivity() {
}
binding.tvHistory.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java))
//startActivity(Intent(this, SellsActivity::class.java))
startSellsActivityWithStatus("all")
}
binding.layoutPerluTagihan.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("pending")
//startActivity(Intent(this, SellsActivity::class.java))
startSellsActivityWithStatus("unpaid")
}
binding.layoutPembayaran.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("paid")
//startActivity(Intent(this, SellsActivity::class.java))
startSellsActivityWithStatus("paid")
}
binding.layoutPerluDikirim.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("processed")
//startActivity(Intent(this, SellsActivity::class.java))
startSellsActivityWithStatus("processed")
}
binding.layoutProductMenu.setOnClickListener {
@ -206,6 +206,22 @@ class MyStoreActivity : AppCompatActivity() {
}
}
private fun startSellsActivityWithStatus(status: String?) {
val intent = Intent(this, SellsActivity::class.java)
intent.putExtra(SellsActivity.EXTRA_INITIAL_STATUS, status)
startActivity(intent)
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
viewModel.getAllStatusCounts()
}
viewModel.loadMyStore()
viewModel.loadMyStoreProducts()
viewModel.fetchBalance()
}
companion object {
private const val PROFILE_REQUEST_CODE = 100
}

View File

@ -21,22 +21,35 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
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.MyStoreRepository
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.BankAdapter
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.SubdsitrictAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.FileUtils
import com.alya.ecommerce_serang.utils.ImageUtils
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.RegisterStoreViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class RegisterStoreActivity : AppCompatActivity() {
@ -48,27 +61,72 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var subdistrictAdapter: SubdsitrictAdapter
private lateinit var bankAdapter: BankAdapter
// pending values (filled from myStoreProfile once)
private var wantedProvinceId: Int? = null
private var wantedCityId: String? = null
private var wantedSubdistrictId: String? = null
private var wantedBankName: String? = null
// one-shot guards so we don't re-apply repeatedly
private var provinceApplied = false
private var cityApplied = false
private var subdistrictApplied = false
private var bankApplied = false
// avoid clearing/overriding while restoring
private var isRestoringSelections = false
// 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 var isReapply: Boolean = false
// Location request code
private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels {
RegisterStoreViewModelFactory(this, intent.extras)
}
private val myStoreViewModel: MyStoreViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.Companion.getApiService(sessionManager)
val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository)
val apiService = ApiConfig.getApiService(sessionManager)
val myStoreRepository = MyStoreRepository(apiService)
MyStoreViewModel(myStoreRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRegisterStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
applyLiveCounter(
binding.etStoreName,
binding.tvCountName,
binding.tvCountNameMax
)
applyLiveCounter(
binding.etStoreDescription,
binding.tvCountDesc,
binding.tvCountDescMax
)
applyLiveCounter(
binding.etStreet,
binding.tvCountStreet,
binding.tvCountStreetMax
)
applyLiveCounter(
binding.etAddressDetail,
binding.tvCountAddressDetail,
binding.tvCountAddressDetailMax
)
sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -89,6 +147,8 @@ class RegisterStoreActivity : AppCompatActivity() {
setupHeader()
isReapply = intent.getBooleanExtra("REAPPLY", false)
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
subdistrictAdapter = SubdsitrictAdapter(this)
@ -101,6 +161,10 @@ class RegisterStoreActivity : AppCompatActivity() {
setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed")
binding.checkboxApprove.setOnCheckedChangeListener { _, _ ->
validateRequiredFields()
}
// Setup observers
setupStoreTypesObserver() // Store type observer
setupObservers()
@ -129,19 +193,182 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.cityId.observe(this) { validateRequiredFields() }
viewModel.storeTypeId.observe(this) { validateRequiredFields() }
// 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()
if (isReapply) {
binding.btnRegister.text = "Ajukan Kembali"
binding.layoutRejected.visibility = View.VISIBLE
myStoreViewModel.loadMyStore()
myStoreViewModel.myStoreProfile.observe(this) { storeDataResponse ->
storeDataResponse?.let { storeResponse ->
val store = storeResponse.store
binding.tvRejectedReason.text = store.approvalReason
// Prefill basic fields
binding.etStoreName.setText(store.storeName)
binding.etStoreDescription.setText(store.storeDescription)
binding.etStreet.setText(store.street)
binding.etPostalCode.setText(store.postalCode)
binding.etAddressDetail.setText(store.detail)
viewModel.storeName.value = store.storeName
viewModel.storeDescription.value = store.storeDescription
viewModel.street.value = store.street
viewModel.postalCode.value = store.postalCode.toIntOrNull() ?: 0
viewModel.addressDetail.value = store.detail
// Prefill bank info
storeResponse.payment.firstOrNull()?.let { payment ->
viewModel.bankName.value = payment.bankName
viewModel.bankNumber.value = payment.bankNum.toIntOrNull() ?: 0
val bankPosition = bankAdapter.findPositionByName(payment.bankName)
binding.spinnerBankName.setSelection(bankPosition, false)
}
// Prefill couriers
storeResponse.shipping.forEach { courier ->
when (courier.courier) {
"jne" -> binding.checkboxJne.isChecked = true
"pos" -> binding.checkboxPos.isChecked = true
"tiki" -> binding.checkboxTiki.isChecked = true
}
}
// Prefill document URIs
store.ktp.let { ktpUri ->
viewModel.ktpUri = ktpUri.toUri()
updateImagePreview(viewModel.ktpUri, binding.imgKtp, binding.layoutUploadKtp)
}
store.npwp.let { npwpUri ->
viewModel.npwpUri = npwpUri.toUri()
updateDocumentPreview(binding.layoutUploadNpwp)
}
store.nib.let { nibUri ->
viewModel.nibUri = nibUri.toUri()
updateDocumentPreview(binding.layoutUploadNib)
}
// Prefill spinner for store types
preselectStoreType(store.storeTypeId)
// Cache what we want to select later (after data arrives)
wantedProvinceId = store.provinceId
wantedCityId = store.cityId
wantedSubdistrictId = store.subdistrict
wantedBankName = storeResponse.payment.firstOrNull()?.bankName
// Cache what we want to select later (after data arrives)
wantedProvinceId = store.provinceId
wantedCityId = store.cityId
wantedSubdistrictId = store.subdistrict
wantedBankName = storeResponse.payment.firstOrNull()?.bankName
// Mark restoring flow on
isRestoringSelections = true
// Try to apply immediately (if adapters already have data), otherwise
// observers below will apply when data is ready.
tryApplyProvince()
tryApplyCity()
tryApplySubdistrict()
tryApplyBank()
validateRequiredFields()
}
}
binding.btnRegister.setOnClickListener {
doUpdateStoreProfile()
}
} else {
binding.btnRegister.setOnClickListener {
if (viewModel.validateForm()){
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah anda yakin ingin mendaftar toko?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.registerStore(this)
}
)
}
else {
Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show()
}
}
}
validateRequiredFields()
}
Log.d(TAG, "onCreate: RegisterStoreActivity setup completed")
private fun preselectStoreType(storeTypeId: Int) {
// The adapter is created in setupStoreTypeSpinner(...)
val adapter = binding.spinnerStoreType.adapter ?: return
for (i in 0 until adapter.count) {
val item = adapter.getItem(i) as? StoreTypesItem
if (item?.id == storeTypeId) {
binding.spinnerStoreType.setSelection(i, false)
viewModel.storeTypeId.value = storeTypeId
validateRequiredFields()
break
}
}
}
private fun tryApplyProvince() {
if (provinceApplied) return
val target = wantedProvinceId ?: return
val count = provinceAdapter.count
for (i in 0 until count) {
if (provinceAdapter.getProvinceId(i) == target) {
binding.spinnerProvince.setSelection(i, false)
provinceApplied = true
maybeFinishRestoring()
return
}
}
}
private fun tryApplyCity() {
if (cityApplied) return
val target = wantedCityId ?: return
val count = cityAdapter.count
for (i in 0 until count) {
if (cityAdapter.getCityId(i) == target) {
binding.spinnerCity.setSelection(i, false)
cityApplied = true
maybeFinishRestoring()
return
}
}
}
private fun tryApplySubdistrict() {
if (subdistrictApplied) return
val target = wantedSubdistrictId ?: return
val count = subdistrictAdapter.count
for (i in 0 until count) {
if (subdistrictAdapter.getSubdistrictId(i) == target) {
binding.spinnerSubdistrict.setSelection(i, false)
subdistrictApplied = true
maybeFinishRestoring()
return
}
}
}
private fun tryApplyBank() {
if (bankApplied) return
val targetName = wantedBankName ?: return
val pos = bankAdapter.findPositionByName(targetName)
if (pos >= 0) {
binding.spinnerBankName.setSelection(pos, false)
viewModel.bankName.value = targetName
viewModel.selectedBankName = targetName
validateRequiredFields()
bankApplied = true
}
}
private fun setupHeader() {
@ -158,30 +385,42 @@ class RegisterStoreActivity : AppCompatActivity() {
}
private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 &&
!viewModel.subdistrict.value.isNullOrBlank() &&
!viewModel.bankName.value.isNullOrBlank() &&
(viewModel.bankNumber.value ?: 0) > 0 &&
(viewModel.provinceId.value ?: 0) > 0 &&
!viewModel.cityId.value.isNullOrBlank() &&
(viewModel.storeTypeId.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank()
val bankName = viewModel.bankName.value?.trim().orEmpty()
val bankSelected = bankName.isNotEmpty() && !bankName.equals("Pilih Bank", ignoreCase = true)
binding.btnRegister.isEnabled = true
if (isFormValid) {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
} else {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
val provinceSelected = viewModel.provinceId.value != null
val citySelected = !viewModel.cityId.value.isNullOrBlank()
val subdistrictSelected = !viewModel.subdistrict.value.isNullOrBlank()
val currentStoreType = binding.spinnerStoreType.selectedItem as? StoreTypesItem
val storeTypeSelected = when {
currentStoreType != null -> currentStoreType.id != 0 &&
!currentStoreType.name.equals("Pilih Jenis UMKM", true)
else -> (viewModel.storeTypeId.value ?: -1) > 0
}
val isFormValid =
!viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 &&
provinceSelected && citySelected && subdistrictSelected &&
storeTypeSelected &&
bankSelected &&
(viewModel.bankNumber.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank() &&
binding.checkboxApprove.isChecked
binding.btnRegister.isEnabled = isFormValid
binding.btnRegister.setBackgroundResource(
if (isFormValid) R.drawable.bg_button_active else R.drawable.bg_button_disabled
)
binding.btnRegister.setTextColor(
ContextCompat.getColor(this, if (isFormValid) R.color.white else R.color.black_300)
)
}
private fun setupObservers() {
@ -196,12 +435,10 @@ class RegisterStoreActivity : AppCompatActivity() {
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)
tryApplyProvince()
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
@ -220,12 +457,10 @@ class RegisterStoreActivity : AppCompatActivity() {
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)
tryApplyCity()
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
@ -243,11 +478,20 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerSubdistrict.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true
subdistrictAdapter.updateData(state.data)
// If youre not restoring a specific subdistrict, select the first real item
if (!isRestoringSelections && state.data.isNotEmpty()) {
binding.spinnerSubdistrict.setSelection(0, false)
val id0 = subdistrictAdapter.getSubdistrictId(0)
viewModel.subdistrict.value = id0 ?: ""
viewModel.selectedSubdistrict = id0
validateRequiredFields()
}
tryApplySubdistrict()
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
@ -302,7 +546,7 @@ class RegisterStoreActivity : AppCompatActivity() {
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()
// Toast.makeText(this, "Error loading store types: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
@ -322,6 +566,7 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup spinner with API data
setupStoreTypeSpinner(displayList)
tryApplyBank()
} else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
}
@ -366,17 +611,10 @@ class RegisterStoreActivity : AppCompatActivity() {
// 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 onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
val item = (binding.spinnerStoreType.adapter.getItem(pos) as? StoreTypesItem)
if (item != null && item.id > 0) viewModel.storeTypeId.value = item.id
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -395,22 +633,12 @@ class RegisterStoreActivity : AppCompatActivity() {
// 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 onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
provinceAdapter.getProvinceId(pos)?.let {
viewModel.provinceId.value = it
viewModel.getCities(it)
}
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -421,21 +649,13 @@ class RegisterStoreActivity : AppCompatActivity() {
// 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
Log.d(TAG, "Fetching subdistrict for city ID: $cityId")
viewModel.getSubdistrict(cityId)
subdistrictAdapter.clear()
binding.spinnerSubdistrict.setSelection(0)
viewModel.selectedCityId = cityId
} else {
Log.e(TAG, "Invalid city ID for position: $position")
override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
cityAdapter.getCityId(pos)?.let {
viewModel.cityId.value = it
viewModel.getSubdistrict(it)
viewModel.selectedCityId = it
}
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -446,16 +666,11 @@ class RegisterStoreActivity : AppCompatActivity() {
//Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position")
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
if (subdistrictId != null) {
Log.d(TAG, "Setting subdistrict ID: $subdistrictId")
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
val selectedId = subdistrictAdapter.getSubdistrictId(pos)
viewModel.subdistrict.value = selectedId ?: "" // empty => not selected
viewModel.selectedSubdistrict = selectedId
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@ -465,63 +680,31 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
Log.d(TAG, "Bank selected at position: $position")
val bankName = bankAdapter.getBankName(position)
if (bankName != null) {
Log.d(TAG, "Setting bank name: $bankName")
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
// Optional: Log the selected bank details
val selectedBank = bankAdapter.getBankItem(position)
selectedBank?.let {
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})")
}
// Hide progress bar if it was showing
binding.bankNameProgressBar.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
val bankName = bankAdapter.getBankName(pos)
viewModel.bankName.value = bankName
viewModel.selectedBankName = bankName
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) { /* no-op */ }
}
tryApplyBank()
// 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")
}
if (subdistrictAdapter.isEmpty) {
Log.d(TAG, "Adding default kecamatan hint")
subdistrictAdapter.add("Pilih Kecamatan")
}
if (bankAdapter.isEmpty) {
Log.d(TAG, "Adding default bank hint")
bankAdapter.add("Pilih Bank")
}
// // Add initial hints to the spinners
// if (provinceAdapter.isEmpty) provinceAdapter.add("Pilih Provinsi")
// if (cityAdapter.isEmpty) cityAdapter.add("Pilih Kabupaten/Kota")
// if (subdistrictAdapter.isEmpty) subdistrictAdapter.add("Pilih Kecamatan")
// if (bankAdapter.isEmpty) bankAdapter.add("Pilih Bank")
Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
}
private fun maybeFinishRestoring() {
if (provinceApplied && cityApplied && subdistrictApplied) {
isRestoringSelections = false
}
}
private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
@ -649,18 +832,28 @@ class RegisterStoreActivity : AppCompatActivity() {
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()
if (viewModel.storeName.value != s.toString()) {
viewModel.storeName.value = s.toString()
}
Log.d(TAG, "Store name updated: ${s.toString()}")
validateRequiredFields()
}
})
viewModel.storeName.observe(this) { value ->
if (binding.etStoreName.text.toString() != value) {
binding.etStoreName.setText(value)
binding.etStoreName.setSelection(value.length) // Set cursor to end
}
}
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 ""}")
validateRequiredFields()
}
})
@ -668,53 +861,80 @@ class RegisterStoreActivity : AppCompatActivity() {
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()
if (viewModel.street.value != s.toString()) {
viewModel.street.value = s.toString()
}
Log.d(TAG, "Street address updated: ${s.toString()}")
validateRequiredFields()
}
})
viewModel.street.observe(this) { value ->
if (binding.etStreet.text.toString() != value) {
binding.etStreet.setText(value)
binding.etStreet.setSelection(value.length)
}
}
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")
validateRequiredFields()
val newValue = s.toString().toIntOrNull() ?: 0
if (viewModel.postalCode.value != newValue) {
viewModel.postalCode.value = newValue
}
validateRequiredFields()
}
})
viewModel.postalCode.observe(this) { value ->
val currentText = binding.etPostalCode.text.toString()
val valueString = if (value == 0) "" else value.toString()
if (currentText != valueString) {
binding.etPostalCode.setText(valueString)
binding.etPostalCode.setSelection(valueString.length)
}
}
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()
if (viewModel.addressDetail.value != s.toString()) {
viewModel.addressDetail.value = s.toString()
}
Log.d(TAG, "Address detail updated: ${s.toString()}")
validateRequiredFields()
}
})
viewModel.addressDetail.observe(this) { value ->
if (binding.etAddressDetail.text.toString() != value) {
binding.etAddressDetail.setText(value)
binding.etAddressDetail.setSelection(value.length)
}
}
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()) {
val newValue = if (input.isNotEmpty()) {
try {
viewModel.bankNumber.value = input.toInt()
Log.d(TAG, "Bank number updated: $input")
input.toInt()
} catch (e: NumberFormatException) {
// Handle invalid input if needed
Log.e(TAG, "Failed to parse bank number. Input: $input, Error: $e")
0
}
} 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")
0
}
if (viewModel.bankNumber.value != newValue) {
viewModel.bankNumber.value = newValue
Log.d(TAG, "Bank number updated: $newValue")
}
validateRequiredFields()
}
@ -744,16 +964,27 @@ class RegisterStoreActivity : AppCompatActivity() {
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.accountName.value = s.toString()
if (viewModel.accountName.value != s.toString()) {
viewModel.accountName.value = s.toString()
}
Log.d(TAG, "Account Name updated: ${s.toString()}")
validateRequiredFields()
}
})
viewModel.accountName.observe(this) { value ->
if (binding.etAccountName.text.toString() != value) {
binding.etAccountName.setText(value)
binding.etAccountName.setSelection(value.length)
}
}
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")
@ -848,6 +1079,133 @@ class RegisterStoreActivity : AppCompatActivity() {
}
}
private fun doUpdateStoreProfile() {
// --- Text parts ---
val nameBody: RequestBody = (viewModel.storeName.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val descBody: RequestBody = (viewModel.storeDescription.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// NOTE: is_on_leave is NOT part of approval multipart; keep separate if needed
val latBody: RequestBody = (viewModel.latitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val longBody: RequestBody = (viewModel.longitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val provBody: RequestBody = ((viewModel.provinceId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val cityBody: RequestBody = (viewModel.cityId.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val subdistrictBody: RequestBody = (viewModel.selectedSubdistrict ?: viewModel.subdistrict.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// If you don't have village picker yet, send empty string or reuse subdistrict
val villageBody: RequestBody = "".toRequestBody("text/plain".toMediaTypeOrNull())
val streetBody: RequestBody = (viewModel.street.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val postalBody: RequestBody = ((viewModel.postalCode.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
val detailBody: RequestBody = (viewModel.addressDetail.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// You can read user phone from current store profile when reapply
val currentPhone = myStoreViewModel.myStoreProfile.value?.store?.userPhone ?: ""
val userPhoneBody: RequestBody = currentPhone.toRequestBody("text/plain".toMediaTypeOrNull())
// --- Multipart images/docs (safe compress/copy) ---
val storeImgPart: MultipartBody.Part? = viewModel.storeImageUri?.let { uri ->
try {
val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE)
if (!ImageUtils.isAllowedFileType(this, uri, allowed)) {
Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show()
null
} else {
val compressed: File = ImageUtils.compressImage(
context = this,
uri = uri,
filename = "storeimg",
maxWidth = 1024,
maxHeight = 1024,
quality = 80
)
FileUtils.createMultipartFromFile("storeimg", compressed)
}
} catch (e: Exception) {
val rawFile = FileUtils.createTempFileFromUri(this, uri)
rawFile?.let { FileUtils.createMultipartFromFile("storeimg", it) }
}
}
val ktpPart: MultipartBody.Part? = viewModel.ktpUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("ktp", it) }
}
val npwpPart: MultipartBody.Part? = viewModel.npwpUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("npwp", it) }
}
val nibPart: MultipartBody.Part? = viewModel.nibUri?.let { uri ->
val file = FileUtils.createTempFileFromUri(this, uri)
file?.let { FileUtils.createMultipartFromFile("nib", it) }
}
// --- Couriers desired (sync to exactly this set) ---
val desiredCouriers = viewModel.selectedCouriers.toList()
// --- (Optional) Payment upsert from UI fields ---
// If you want to send the bank from the form during re-apply:
val paymentsToUpsert = buildList {
val bankName = viewModel.bankName.value
val bankNum = viewModel.bankNumber.value?.toString()
val accName = viewModel.accountName.value
if (!bankName.isNullOrBlank() && !bankNum.isNullOrBlank() && !accName.isNullOrBlank()) {
// If you want to update the first existing payment instead of adding new:
val existingId = myStoreViewModel.payment.value?.firstOrNull()?.id
add(
PaymentUpdate(
id = existingId, // null => add; id!=null => update
bankName = bankName,
bankNum = bankNum,
accountName = accName,
qrisImage = null // attach File if you have new QRIS to upload
)
)
}
}
// --- Delete list (empty if none) ---
val paymentIdToDelete = emptyList<Int>()
// --- Fire the update ---
myStoreViewModel.updateStoreApproval(
storeName = nameBody,
description = descBody,
storeType = typeBody,
latitude = latBody,
longitude = longBody,
storeProvince = provBody,
storeCity = cityBody,
storeSubdistrict = subdistrictBody,
storeVillage = villageBody,
storeStreet = streetBody,
storePostalCode = postalBody,
storeAddressDetail = detailBody,
userPhone = userPhoneBody,
paymentsToUpdate = paymentsToUpsert,
paymentIdToDelete = paymentIdToDelete,
storeCourier = desiredCouriers,
storeImage = storeImgPart,
ktpImage = ktpPart,
npwpDocument = npwpPart,
nibDocument = nibPart
)
myStoreViewModel.updateStoreProfileResult.observe(this) {
Toast.makeText(this, "Pengajuan ulang berhasil dikirim", Toast.LENGTH_SHORT).show()
finish()
}
myStoreViewModel.errorMessage.observe(this) {
if (!it.isNullOrEmpty()) {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
}
}
}
companion object {
private const val TAG = "RegisterStoreActivity"
}

View File

@ -24,6 +24,7 @@ import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -374,14 +375,11 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the success message
runOnUiThread {
AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Berhasil")
.setMessage(successMessage)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
finish()
}
.show()
PopUpDialog.showConfirmDialog(
context = this@BalanceTopUpActivity,
iconRes = R.drawable.checkmark__1_,
title = "Berhasil melakukan Top-Up"
)
}
} else {
// Get more detailed error information
@ -408,13 +406,12 @@ class BalanceTopUpActivity : AppCompatActivity() {
// Show a dialog with the error message
runOnUiThread {
AlertDialog.Builder(this@BalanceTopUpActivity)
.setTitle("Error Response")
.setMessage(errorMessage)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
.show()
PopUpDialog.showConfirmDialog(
context = this@BalanceTopUpActivity,
iconRes = R.drawable.ic_cancel,
title = "Gagal melakukan Top-Up"
)
}
}
} catch (e: Exception) {

View File

@ -27,9 +27,11 @@ import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.FileUtils.compressFile
import com.alya.ecommerce_serang.utils.CompressionResult
import com.alya.ecommerce_serang.utils.FileUtils.compressFileToMax1MB
import com.alya.ecommerce_serang.utils.ImageUtils.compressImage
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -49,6 +51,11 @@ class DetailStoreProductActivity : AppCompatActivity() {
private var productId: Int? = null
private var hasImage: Boolean = false
private var TAG="DetailStoreProduct"
private var isEditing = false
private var hasExistingImage = false
private val viewModel: ProductViewModel by viewModels {
BaseViewModelFactory {
sessionManager = SessionManager(this)
@ -75,20 +82,32 @@ class DetailStoreProductActivity : AppCompatActivity() {
private val sppirtLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) {
compressFile(this, uri).let { compressedFile ->
sppirtUri = compressedFile?.toUri()
binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext()
when (val result = compressFileToMax1MB(this, uri)) {
is CompressionResult.Success -> {
sppirtUri = result.file.toUri()
binding.tvSppirtName.text = getFileName(sppirtUri!!)
binding.switcherSppirt.showNext()
}
is CompressionResult.Error -> {
Toast.makeText(this, result.reason, Toast.LENGTH_LONG).show()
Log.e(TAG, "Compression failed: ${result.reason}")
}
}
}
}
private val halalLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri != null && isValidFile(uri)) {
compressFile(this, uri).let { compressedFile ->
halalUri = compressedFile?.toUri()
binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext()
when (val result = compressFileToMax1MB(this, uri)) {
is CompressionResult.Success -> {
halalUri = result.file.toUri()
binding.tvHalalName.text = getFileName(halalUri!!)
binding.switcherHalal.showNext()
}
is CompressionResult.Error -> {
Toast.makeText(this, result.reason, Toast.LENGTH_LONG).show()
Log.e(TAG, "Compression failed: ${result.reason}")
}
}
}
}
@ -98,7 +117,19 @@ class DetailStoreProductActivity : AppCompatActivity() {
binding = ActivityDetailStoreProductBinding.inflate(layoutInflater)
setContentView(binding.root)
val isEditing = intent.getBooleanExtra("is_editing", false)
applyLiveCounter(
binding.edtNamaProduk,
binding.tvCountName,
binding.tvCountNameMax
)
applyLiveCounter(
binding.edtDeskripsiProduk,
binding.tvCountDesc,
binding.tvCountDescMax
)
isEditing = intent.getBooleanExtra("is_editing", false)
productId = intent.getIntExtra("product_id", -1)
binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
@ -179,6 +210,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
binding.btnRemoveFoto.setOnClickListener {
imageUri = null
hasImage = false
hasExistingImage = false
binding.switcherFotoProduk.showPrevious()
validateForm()
}
@ -236,7 +268,9 @@ class DetailStoreProductActivity : AppCompatActivity() {
} else product.image
Glide.with(this).load(imageUrl).into(binding.ivPreviewFoto)
binding.switcherFotoProduk.showNext()
hasImage = true
hasExistingImage = true
// SPPIRT
product.sppirt?.let {
@ -250,16 +284,19 @@ class DetailStoreProductActivity : AppCompatActivity() {
binding.switcherHalal.showNext()
}
binding.switchIsActive.isChecked = product.status == "active"
binding.switchIsActive.jumpDrawablesToCurrentState()
validateForm()
}
private fun togglePreOrderVisibility(isChecked: Boolean) {
Log.d("DEBUG", "togglePreOrderVisibility: $isChecked")
Log.d(TAG, "togglePreOrderVisibility: $isChecked")
binding.layoutDurasi.visibility = if (isChecked) View.VISIBLE else View.GONE
}
private fun toggleWholesaleVisibility(isChecked: Boolean) {
Log.d("DEBUG", "toggleWholesaleVisibility: $isChecked")
Log.d(TAG, "toggleWholesaleVisibility: $isChecked")
binding.layoutMinPesanGrosir.visibility = if (isChecked) View.VISIBLE else View.GONE
binding.layoutHargaGrosir.visibility = if (isChecked) View.VISIBLE else View.GONE
}
@ -331,10 +368,18 @@ class DetailStoreProductActivity : AppCompatActivity() {
val duration = binding.edtDurasi.text.toString().trim()
val wholesaleMinItem = binding.edtMinPesanGrosir.text.toString().trim()
val wholesalePrice = binding.edtHargaGrosir.text.toString().trim()
val category = binding.spinnerKategoriProduk.selectedItemPosition != -1
val categorySelected = binding.spinnerKategoriProduk.selectedItemPosition != -1
val isPreOrderChecked = binding.switchIsPreOrder.isChecked
val isWholesaleChecked = binding.switchIsWholesale.isChecked
val hasRequiredImage = if (isEditing) {
// In edit mode: allow existing server image OR newly picked image
hasImage || hasExistingImage
} else {
// In create mode: must have a picked image
hasImage
}
val valid = name.isNotEmpty() &&
description.isNotEmpty() &&
price.isNotEmpty() &&
@ -343,8 +388,8 @@ class DetailStoreProductActivity : AppCompatActivity() {
weight.isNotEmpty() &&
(!isPreOrderChecked || duration.isNotEmpty()) &&
(!isWholesaleChecked || (wholesaleMinItem.isNotEmpty() && wholesalePrice.isNotEmpty())) &&
category &&
hasImage
categorySelected &&
hasRequiredImage
binding.btnSaveProduct.isEnabled = valid
binding.btnSaveProduct.setTextColor(
@ -387,8 +432,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
val sppirtFile = sppirtUri?.let { uriToNamedFile(it, this, "sppirt") }
val halalFile = halalUri?.let { uriToNamedFile(it, this, "halal") }
Log.d("File URI", "SPPIRT URI: ${sppirtUri.toString()}")
Log.d("File URI", "Halal URI: ${halalUri.toString()}")
Log.d(TAG, "SPPIRT URI: ${sppirtUri.toString()}")
Log.d(TAG, "Halal URI: ${halalUri.toString()}")
sppirtFile?.let { logFileInfo("Sppirt Size", it) }
halalFile?.let { logFileInfo("Halal Size", it) }
val imagePart = imageFile?.let { createPartFromFile("productimg", it) }
val sppirtPart = sppirtFile?.let { createPartFromFile("sppirt", it) }
@ -414,7 +461,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
finish()
}
is Result.Error -> {
Log.e("ProductDetailActivity", "Error: ${result.exception.message}")
Log.e(TAG, "Error: ${result.exception.message}")
binding.btnSaveProduct.isEnabled = true
}
}
@ -492,7 +539,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
finish()
}
is Result.Error -> {
Log.e("ProductDetailActivity", "Error: ${result.exception.message}")
Log.e(TAG, "Error: ${result.exception.message}")
binding.btnSaveProduct.isEnabled = true
}
}
@ -501,4 +548,10 @@ class DetailStoreProductActivity : AppCompatActivity() {
private fun toRequestBody(value: String): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), value)
private fun logFileInfo(tag: String, file: File) {
val sizeKb = file.length() / 1024.0
val sizeMb = sizeKb / 1024.0
Log.d(TAG, "$tag → Name: ${file.name}, Size: ${"%.2f".format(sizeKb)} KB (${String.format("%.2f", sizeMb)} MB)")
}
}

View File

@ -29,9 +29,10 @@ import com.alya.ecommerce_serang.databinding.DialogStoreImageBinding
import com.alya.ecommerce_serang.ui.profile.mystore.profile.address.DetailStoreAddressActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.payment_info.PaymentInfoActivity
import com.alya.ecommerce_serang.ui.profile.mystore.profile.shipping_service.ShippingServiceActivity
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
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.viewmodel.MyStoreViewModel
import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
@ -39,7 +40,6 @@ import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import java.io.FileOutputStream
import kotlin.getValue
class DetailStoreProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailStoreProfileBinding
@ -91,10 +91,10 @@ class DetailStoreProfileActivity : AppCompatActivity() {
viewModel.fetchStoreTypes()
viewModel.myStoreProfile.observe(this) {
currentStore = it
currentStore = it?.store
currentStoreLoaded = true
if (storeTypesLoaded) setupStoreTypeSpinner(storeTypesList)
updateUI(it)
updateUI(it?.store)
}
viewModel.storeTypes.observe(this) {
@ -244,12 +244,17 @@ class DetailStoreProfileActivity : AppCompatActivity() {
}
private fun confirmUpdate() {
AlertDialog.Builder(this)
.setTitle("Konfirmasi Perubahan")
.setMessage("Apakah Anda yakin ingin menyimpan perubahan profil toko Anda?")
.setPositiveButton("Ya") { _, _ -> updateStoreProfile() }
.setNegativeButton("Batal", null)
.show()
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah Anda yakin ingin menyimpan perubahan profil?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
updateStoreProfile()
}
)
}
private fun showImageOptions() {

View File

@ -22,6 +22,7 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
import com.google.android.material.snackbar.Snackbar
@ -55,6 +56,18 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding = ActivityDetailStoreAddressBinding.inflate(layoutInflater)
setContentView(binding.root)
applyLiveCounter(
binding.edtStreet,
binding.tvCountStreet,
binding.tvCountStreetMax
)
applyLiveCounter(
binding.edtDetailAddress,
binding.tvCountDetail,
binding.tvCountDetailMax
)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
@ -322,19 +335,6 @@ class DetailStoreAddressActivity : AppCompatActivity() {
villageId = oldAddress.villageId
)
viewModel.saveStoreAddress(oldAddress, newAddress)
// Save address
// viewModel.saveStoreAddress(
// provinceId = selectedProvinceId!!,
// provinceName = province?.provinceName ?: "",
// cityId = city.cityId,
// cityName = city.cityName,
// street = street,
// subdistrict = subdistrict,
// detail = detail,
// postalCode = postalCode,
// latitude = latitude,
// longitude = longitude
// )
}
}
@ -360,11 +360,13 @@ class DetailStoreAddressActivity : AppCompatActivity() {
it.isEnabled = true
it.setBackgroundResource(R.drawable.bg_button_active)
it.setTextColor(getColor(R.color.white))
binding.btnSaveAddress.text = "Simpan Perubahan"
} else {
it.isEnabled = false
it.setBackgroundResource(R.drawable.bg_button_disabled)
it.setTextColor(getColor(R.color.black_300))
Toast.makeText(this, "Periksa dan lenkapi alamat anda", Toast.LENGTH_SHORT).show()
binding.btnSaveAddress.text = "Lengkapi alamat anda"
}
}
}

View File

@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.profile.mystore.sells
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
@ -12,7 +11,6 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.AddressRepository
import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailSellsBinding
import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmentActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
@ -22,7 +20,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.getValue
class DetailSellsActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailSellsBinding
@ -100,8 +97,9 @@ class DetailSellsActivity : AppCompatActivity() {
tvOrderCustomer.text = sell.username
tvOrderDate.text = formatDate(sell.updatedAt.toString())
tvOrderTotalProduct.text = "(${sell.orderItems?.size ?: 0} Barang)"
tvOrderSubtotal.text = formatPrice(sell.totalAmount.toString())
tvOrderShipPrice.text = formatPrice(sell.shipmentPrice.toString())
val totalPrice = (sell.totalAmount?.toDouble()?.toInt() ?: 0) - (sell.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvOrderSubtotal.text = formatPrice(totalPrice.toString())
tvOrderPrice.text = formatPrice(sell.totalAmount.toString())
tvOrderRecipient.text = sell.recipient ?: "-"
tvOrderRecipientNum.text = sell.receiptNum?.toString() ?: "-"

View File

@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.commit
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
@ -18,6 +19,9 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
class SellsActivity : AppCompatActivity() {
companion object {
const val EXTRA_INITIAL_STATUS = "extra_initial_status"
}
private lateinit var binding: ActivitySellsBinding
private lateinit var sessionManager: SessionManager
@ -68,11 +72,18 @@ class SellsActivity : AppCompatActivity() {
onBackPressed()
finish()
}
// binding.edtSearch.doAfterTextChanged {
// val q = it?.toString()?.trim().orEmpty()
// (supportFragmentManager.findFragmentById(R.id.fragment_container_sells) as? SellsFragment)
// ?.onSearchQueryChanged(q)
// }
}
private fun showSellsFragment() {
val initialStatus = intent.getStringExtra(EXTRA_INITIAL_STATUS)
supportFragmentManager.commit {
replace(R.id.fragment_container_sells, SellsFragment())
replace(R.id.fragment_container_sells, SellsFragment.newInstance(initialStatus))
}
}
}

View File

@ -17,6 +17,7 @@ import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmen
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.bumptech.glide.Glide
import com.google.gson.Gson
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -96,7 +97,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString())
tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toDouble().toInt()) }
val fullImageUrl = when (val img = product?.productImage) {
is String -> {
@ -169,7 +170,7 @@ class SellsAdapter(
val product = order.orderItems?.firstOrNull()
tvSellsProductName.text = product?.productName
tvSellsProductQty.text = "x${product?.quantity}"
tvSellsProductPrice.text = formatPrice(product?.price.toString())
tvSellsProductPrice.text = product?.price?.let { formatPrice(it.toDouble().toInt()) }
val fullImageUrl = when (val img = product?.productImage) {
is String -> {
@ -185,7 +186,9 @@ class SellsAdapter(
.into(ivSellsProduct)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsPrice.text = formatPrice(order.totalAmount.toString())
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
Log.d("SellsAdapter", "Cek price:$totalPrice" )
}
"paid" -> {
layoutOrders.visibility = View.GONE
@ -205,7 +208,9 @@ class SellsAdapter(
tvSellsTitle.text = "Pesanan Telah Dibayar"
tvSellsDueDesc.text = "Konfirmasi pembayaran sebelum:"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 2)
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
tvSellsQty.text = "${order.orderItems?.size} produk"
}
"processed" -> {
layoutOrders.visibility = View.GONE
@ -234,6 +239,10 @@ class SellsAdapter(
tvSellsLocation.text = order.subdistrict
tvSellsCustomer.text = order.username
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 2)
tvSellsQty.text = "${order.orderItems?.size} produk"
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
}
"shipped" -> {
layoutOrders.visibility = View.GONE
@ -245,7 +254,11 @@ class SellsAdapter(
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
tvSellsQty.text = "${order.orderItems?.size} produk"
btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
}
"delivered" -> {
layoutOrders.visibility = View.GONE
@ -256,8 +269,13 @@ class SellsAdapter(
tvSellsDueDesc.text = "Dikirimkan pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
}
"completed" -> {
layoutOrders.visibility = View.GONE
@ -268,8 +286,13 @@ class SellsAdapter(
tvSellsDueDesc.text = "Selesai pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
}
"canceled" -> {
layoutOrders.visibility = View.GONE
@ -280,6 +303,10 @@ class SellsAdapter(
tvSellsDueDesc.text = "Dibatalkan pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsQty.text = "${order.orderItems?.size} produk"
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE
}
@ -308,10 +335,9 @@ class SellsAdapter(
}
}
private fun formatPrice(price: String): String {
val priceDouble = price.toDoubleOrNull() ?: 0.0
val formattedPrice = String.format(Locale("id", "ID"), "Rp%,.0f", priceDouble)
return formattedPrice
private fun formatPrice(amount: Int): String {
val formatter = NumberFormat.getCurrencyInstance(Locale("in", "ID"))
return formatter.format(amount.toLong()).replace(",00", "")
}
}
}

View File

@ -5,19 +5,43 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.FragmentSellsBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.google.android.material.tabs.TabLayoutMediator
class SellsFragment : Fragment() {
companion object {
private const val ARG_INITIAL_STATUS = "arg_initial_status"
fun newInstance(initialStatus: String?): SellsFragment =
SellsFragment().apply {
arguments = Bundle().apply {
putString(ARG_INITIAL_STATUS, initialStatus)
}
}
}
private var _binding: FragmentSellsBinding? = null
// private var currentSearchQuery = ""
private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager
private lateinit var viewPagerAdapter: SellsViewPagerAdapter
private val sellsVm: SellsViewModel by activityViewModels {
BaseViewModelFactory {
val api = ApiConfig.getApiService(SessionManager(requireContext()))
SellsViewModel(SellsRepository(api))
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -32,6 +56,8 @@ class SellsFragment : Fragment() {
sessionManager = SessionManager(requireContext())
setupViewPager()
jumpToInitialStatusIfAny()
// binding.viewPagerSells.post { currentPage()?.filter(currentSearchQuery) }
}
private fun setupViewPager() {
@ -53,8 +79,73 @@ class SellsFragment : Fragment() {
else -> "Tab $position"
}
}.attach()
statusPage()
}
private fun jumpToInitialStatusIfAny() {
val initial = arguments?.getString(ARG_INITIAL_STATUS)?.trim().orEmpty()
if (initial.isEmpty()) return
// Try adapters list first
var index = viewPagerAdapter.sellsStatuses.indexOf(initial)
// Fallback mapping (keeps working if the adapter changes order names)
if (index < 0) {
index = when (initial) {
"all" -> 0
"unpaid" -> 1
"paid" -> 2
"processed" -> 3
"shipped" -> 4
"delivered" -> 5
"completed" -> 6
"canceled" -> 7
else -> 0
}
}
if (index in 0 until (binding.viewPagerSells.adapter?.itemCount ?: 0)) {
// Ensure pager is ready, then jump without animation
binding.viewPagerSells.post {
binding.viewPagerSells.setCurrentItem(index, false)
// Make sure ViewModel filter matches the shown tab
sellsVm.updateStatus(
viewPagerAdapter.sellsStatuses.getOrNull(index) ?: initial,
forceRefresh = true
)
}
}
}
// fun onSearchQueryChanged(q: String) {
// currentSearchQuery = q
// currentPage()?.filter(q)
// }
// private fun currentPage(): SellsListFragment? {
// val pos = binding.viewPagerSells.currentItem
// val tag = "f${viewPagerAdapter.getItemId(pos)}" // requires stable ids in adapter (see step 4)
// return childFragmentManager.findFragmentByTag(tag) as? SellsListFragment
// }
private fun statusPage() {
binding.viewPagerSells.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val status = viewPagerAdapter.sellsStatuses[position]
sellsVm.updateStatus(status, forceRefresh = true)
// re-apply query when user switches tab
// currentPage()?.filter(currentSearchQuery)
}
}
)
}
override fun onResume() {
super.onResume()
statusPage()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

View File

@ -25,6 +25,7 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.google.gson.Gson
import kotlinx.coroutines.launch
import java.util.Locale
class SellsListFragment : Fragment() {
@ -41,6 +42,13 @@ class SellsListFragment : Fragment() {
}
private lateinit var sellsAdapter: SellsAdapter
private var status: String = "all"
// private var allOrders: List<OrdersItem> = emptyList()
// private var currentQuery: String = ""
//
// fun filter(query: String) {
// currentQuery = query
// applyFilter()
// }
companion object {
private const val TAG = "SellsListFragment"
@ -83,13 +91,35 @@ class SellsListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadSells()
setupRecyclerView()
observeSellsList()
observePaymentConfirmation()
loadSells()
// getAllOrderCountsAndNavigate()
}
// private fun applyFilter() {
// val q = currentQuery.lowercase(Locale.getDefault())
// val filtered = if (q.isBlank()) {
// allOrders
// } else {
// allOrders.filter { it.matches(q) }
// }
//
// sellsAdapter.submitList(filtered)
// binding.tvEmptyState.visibility = if (filtered.isEmpty()) View.VISIBLE else View.GONE
// binding.rvSells.visibility = if (filtered.isEmpty()) View.GONE else View.VISIBLE
// }
//
// private fun OrdersItem.matches(q: String): Boolean {
// val id = orderId?.toString()?.lowercase(Locale.getDefault()) ?: ""
// val customer = username?.lowercase(Locale.getDefault()) ?: ""
// val location = subdistrict?.lowercase(Locale.getDefault()) ?: ""
// val productHit = orderItems?.any { it?.productName
// ?.lowercase(Locale.getDefault())?.contains(q) == true } == true
//
// return id.contains(q) || customer.contains(q) || location.contains(q) || productHit
// }
private fun setupRecyclerView() {
Log.d(TAG, "Setting up RecyclerView")
sellsAdapter = SellsAdapter(
@ -119,10 +149,12 @@ class SellsListFragment : Fragment() {
is ViewState.Success -> {
binding.progressBar.visibility = View.GONE
Log.d(TAG, "Data received: ${result.data?.size ?: 0} items")
// allOrders = result.data ?: emptyList()
// applyFilter() // ← apply current query to fresh data
if (result.data.isNullOrEmpty()) {
binding.tvEmptyState.visibility = View.VISIBLE
binding.rvSells.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE
Log.d(TAG, "Showing empty state")
} else {
@ -136,18 +168,17 @@ class SellsListFragment : Fragment() {
sellsAdapter.submitList(result.data)
Log.d(TAG, "Data submitted to adapter")
Log.d(TAG, "Adapter item count: ${sellsAdapter.itemCount}") }
Log.d(TAG, "Adapter item count: ${sellsAdapter.itemCount}")
}
}
is ViewState.Error -> {
Log.e(TAG, "❌ ViewState.Error received: ${result.message}")
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
// Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
}
is ViewState.Loading -> binding.progressBar.visibility = View.VISIBLE
}
}
}
@ -211,6 +242,12 @@ class SellsListFragment : Fragment() {
}
}
override fun onResume() {
super.onResume()
viewModel.getSellList(status)
//observeSellsList()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

View File

@ -9,7 +9,7 @@ class SellsViewPagerAdapter(
) : FragmentStateAdapter(fragmentActivity) {
// Define all possible sells statuses - keeping your original list
private val sellsStatuses = listOf(
val sellsStatuses = listOf(
"all",
"unpaid",
"paid",
@ -26,4 +26,7 @@ class SellsViewPagerAdapter(
// Create a new instance of SellsListFragment with the appropriate status
return SellsListFragment.newInstance(sellsStatuses[position])
}
// override fun getItemId(position: Int): Long = sellsStatuses[position].hashCode().toLong()
// override fun containsItem(itemId: Long): Boolean =
// sellsStatuses.any { it.hashCode().toLong() == itemId }
}

View File

@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.profile.mystore.sells.payment
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
@ -16,6 +15,8 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.sells.Orders
@ -26,6 +27,7 @@ import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailPaymentBinding
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter
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.viewmodel.AddressViewModel
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
@ -38,9 +40,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import androidx.core.graphics.drawable.toDrawable
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.ui.profile.mystore.sells.DetailSellsActivity
class DetailPaymentActivity : AppCompatActivity() {
@ -111,8 +110,18 @@ class DetailPaymentActivity : AppCompatActivity() {
binding.btnConfirmPayment.setOnClickListener {
sells?.orderId?.let {
viewModel.confirmPayment(it, "confirmed")
Toast.makeText(this, "Pembayaran dikonfirmasi", Toast.LENGTH_SHORT).show()
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah anda yakin?",
message = "Pastikan data pembayaran sudah sesuai",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.confirmPayment(it, "confirmed")
Toast.makeText(this, "Pembayaran dikonfirmasi", Toast.LENGTH_SHORT).show()
}
)
} ?: run {
Log.e("DetailPaymentActivity", "No order passed in intent")
}
@ -136,9 +145,11 @@ class DetailPaymentActivity : AppCompatActivity() {
tvOrderCustomer.text = sell.username
tvOrderDate.text = formatDate(sell.updatedAt.toString())
tvOrderTotalProduct.text = "(${sell.orderItems?.size ?: 0} Barang)"
tvOrderSubtotal.text = formatPrice(sell.totalAmount.toString())
val totalPrice = (sell.totalAmount?.toDouble()?.toInt() ?: 0) - (sell.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvOrderSubtotal.text = formatPrice(totalPrice.toString())
tvOrderShipPrice.text = formatPrice(sell.shipmentPrice.toString())
tvOrderPrice.text = formatPrice(sell.totalAmount.toString())
tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2)
tvOrderRecipient.text = sell.recipient ?: "-"
tvOrderRecipientNum.text = sell.receiptNum?.toString() ?: "-"
@ -201,6 +212,28 @@ class DetailPaymentActivity : AppCompatActivity() {
}
}
private fun formatDueDate(dateString: String, dueDay: Int): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val outputFormat = SimpleDateFormat("dd MMM; HH.mm", Locale("id", "ID"))
val date = inputFormat.parse(dateString)
date?.let {
val calendar = Calendar.getInstance()
calendar.time = it
calendar.add(Calendar.DATE, dueDay)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
Log.e("DateFormatting", "Error formatting date: ${e.message}")
dateString
}
}
private fun formatPrice(price: String): String {
val priceDouble = price.toDoubleOrNull() ?: 0.0
return String.format(Locale("id", "ID"), "Rp%,.0f", priceDouble)

View File

@ -14,7 +14,6 @@ import com.alya.ecommerce_serang.data.repository.AddressRepository
import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailShipmentBinding
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter
import com.alya.ecommerce_serang.ui.profile.mystore.sells.payment.DetailPaymentActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
@ -24,7 +23,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.getValue
class DetailShipmentActivity : AppCompatActivity() {
@ -103,9 +101,11 @@ class DetailShipmentActivity : AppCompatActivity() {
tvOrderCustomer.text = sell.username
tvOrderDate.text = formatDate(sell.updatedAt.toString())
tvOrderTotalProduct.text = "(${sell.orderItems?.size ?: 0} Barang)"
tvOrderSubtotal.text = formatPrice(sell.totalAmount.toString())
val totalPrice = (sell.totalAmount?.toDouble()?.toInt() ?: 0) - (sell.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvOrderSubtotal.text = formatPrice(totalPrice.toString())
tvOrderShipPrice.text = formatPrice(sell.shipmentPrice.toString())
tvOrderPrice.text = formatPrice(sell.totalAmount.toString())
tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2)
tvOrderRecipient.text = sell.recipient ?: "-"
tvOrderRecipientNum.text = sell.receiptNum?.toString() ?: "-"
@ -168,6 +168,28 @@ class DetailShipmentActivity : AppCompatActivity() {
}
}
private fun formatDueDate(dateString: String, dueDay: Int): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
val outputFormat = SimpleDateFormat("dd MMM; HH.mm", Locale("id", "ID"))
val date = inputFormat.parse(dateString)
date?.let {
val calendar = Calendar.getInstance()
calendar.time = it
calendar.add(Calendar.DATE, dueDay)
outputFormat.format(calendar.time)
} ?: dateString
} catch (e: Exception) {
Log.e("DateFormatting", "Error formatting date: ${e.message}")
dateString
}
}
private fun formatPrice(price: String): String {
val priceDouble = price.toDoubleOrNull() ?: 0.0
return String.format(Locale("id", "ID"), "Rp%,.0f", priceDouble)

View File

@ -4,6 +4,9 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.store.sells.Orders
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.SellsRepository
@ -47,6 +50,8 @@ class ShipmentConfirmationActivity : AppCompatActivity() {
binding.edtKurir.setText(sells?.courier ?: "")
binding.edtLayananKirim.setText(sells?.service ?: "")
setupValidation()
binding.btnConfirm.setOnClickListener {
val receiptNum = binding.edtNoResi.text.toString().trim()
val orderId = sells?.orderId
@ -68,4 +73,32 @@ class ShipmentConfirmationActivity : AppCompatActivity() {
if (success) finish()
}
}
private fun setupValidation() {
// Re-validate whenever any field changes
listOf(
binding.edtKurir,
binding.edtLayananKirim,
binding.edtNoResi
).forEach { edit ->
edit.doAfterTextChanged { validateForm() }
}
// Initial state
validateForm()
}
private fun validateForm() {
val allFilled = binding.edtKurir.text?.toString()?.trim()?.isNotEmpty() == true &&
binding.edtLayananKirim.text?.toString()?.trim()?.isNotEmpty() == true &&
binding.edtNoResi.text?.toString()?.trim()?.isNotEmpty() == true
binding.btnConfirm.isEnabled = allFilled
binding.btnConfirm.setBackgroundResource(
if (allFilled) R.drawable.bg_button_active else R.drawable.bg_button_disabled
)
binding.btnConfirm.setTextColor(
ContextCompat.getColor(this, if (allFilled) R.color.white else R.color.black_300)
)
}
}

View File

@ -1,10 +1,16 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.savedstate.SavedStateRegistryOwner
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
class BaseViewModelFactory<VM : ViewModel>(
private val creator: () -> VM
@ -30,3 +36,29 @@ class SavedStateViewModelFactory<VM : ViewModel>(
return creator(handle) as T
}
}
class RegisterStoreViewModelFactory(
private val owner: SavedStateRegistryOwner,
private val defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
return when {
modelClass.isAssignableFrom(RegisterStoreViewModel::class.java) -> {
// Create SessionManager and ApiService
val context = if (owner is Context) owner else (owner as Fragment).requireContext()
val sessionManager = SessionManager(context)
val apiService = ApiConfig.getApiService(sessionManager)
val repository = UserRepository(apiService)
RegisterStoreViewModel(repository, handle) as T
}
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
}
}

View File

@ -1,12 +1,17 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import com.tom_roush.pdfbox.pdmodel.PDDocument
import com.tom_roush.pdfbox.rendering.PDFRenderer
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
@ -135,4 +140,115 @@ object FileUtils {
else -> "application/octet-stream"
}
}
// for Uri format
// fun compressFileToMax1MB(context: Context, uri: Uri): File? {
// val mimeType = context.contentResolver.getType(uri)
//
// return if (mimeType?.startsWith("image/") == true) {
// // Handle images (jpg/png)
// compressImageToMax1MB(context, uri)
// } else if (mimeType == "application/pdf") {
// // Handle PDFs
// val file = createTempFileFromUri(context, uri, "pdf")
// return if (file != null && file.length() <= 1_048_576) {
// file
// } else {
// // 🚨 Without a PDF compression lib, you can only reject if > 1 MB
// null
// }
// } else {
// // Unsupported type
// null
// }
// }
fun compressFileToMax1MB(context: Context, uri: Uri): CompressionResult {
val mimeType = context.contentResolver.getType(uri) ?: return CompressionResult.Error("Tipe file tidak diketahui")
return if (mimeType.startsWith("image/")) {
val compressed = compressImageToMax1MB(context, uri)
if (compressed != null) {
CompressionResult.Success(compressed)
} else {
CompressionResult.Error("Ukuran gambar terlalu besar. Max 1MB.")
}
} else if (mimeType == "application/pdf") {
val file = createTempFileFromUri(context, uri, "pdf")
if (file == null) {
return CompressionResult.Error("Tidak bisa membaca file pdf")
}
if (file.length() <= 1_048_576) {
return CompressionResult.Success(file)
}
val compressed = compressPdfToMax1MB(context, file)
if (compressed != null) {
CompressionResult.Success(compressed)
} else {
CompressionResult.Error("Ukuran pdf terlalu besar. Max 1MB.")
}
} else {
CompressionResult.Error("Tipe file tidak didukung: $mimeType")
}
}
fun compressImageToMax1MB(context: Context, uri: Uri): File? {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val originalBitmap = BitmapFactory.decodeStream(inputStream)
var quality = 100
var compressedFile: File
var outputStream: ByteArrayOutputStream
do {
outputStream = ByteArrayOutputStream()
originalBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
val compressedBytes = outputStream.toByteArray()
compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.jpg")
FileOutputStream(compressedFile).use { it.write(compressedBytes) }
quality -= 5
} while (compressedFile.length() > 1_048_576 && quality > 10)
return if (compressedFile.length() <= 1_048_576) compressedFile else null
}
fun compressPdfToMax1MB(context: Context, inputFile: File): File? {
return try {
val document = PDDocument.load(inputFile)
val renderer = PDFRenderer(document)
val compressedDoc = PDDocument()
for (pageIndex in 0 until document.numberOfPages) {
val bitmap = renderer.renderImageWithDPI(pageIndex, 72f) // low DPI → smaller size
val outPage = com.tom_roush.pdfbox.pdmodel.PDPage(
com.tom_roush.pdfbox.pdmodel.common.PDRectangle(bitmap.width.toFloat(), bitmap.height.toFloat())
)
compressedDoc.addPage(outPage)
val pdImage = com.tom_roush.pdfbox.pdmodel.graphics.image.LosslessFactory.createFromImage(compressedDoc, bitmap)
val contentStream = com.tom_roush.pdfbox.pdmodel.PDPageContentStream(compressedDoc, outPage)
contentStream.drawImage(pdImage, 0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat())
contentStream.close()
}
val compressedFile = File(context.cacheDir, "compressed_${System.currentTimeMillis()}.pdf")
compressedDoc.save(compressedFile)
compressedDoc.close()
document.close()
// Check size
return if (compressedFile.length() <= 1_048_576) compressedFile else null
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
sealed class CompressionResult {
data class Success(val file: File) : CompressionResult()
data class Error(val reason: String) : CompressionResult()
}

View File

@ -1,8 +1,12 @@
package com.alya.ecommerce_serang.utils
import android.os.Build
import android.text.InputFilter
import android.view.View
import android.view.WindowInsetsController
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
fun Fragment.setLightStatusBar(){
@ -20,3 +24,26 @@ fun Fragment.setLightStatusBar(){
}
}
public fun applyLiveCounter(
editText: EditText,
tvCount: TextView,
tvMax: TextView
) {
val max = tvMax.text.toString().toIntOrNull() ?: Int.MAX_VALUE
// Replace any existing LengthFilter with the new max
val current = editText.filters?.toMutableList() ?: mutableListOf()
current.removeAll { it is InputFilter.LengthFilter }
current.add(InputFilter.LengthFilter(max))
editText.filters = current.toTypedArray()
// Set initial count (handles prefilled text / edit mode)
tvCount.text = (editText.text?.length ?: 0).toString()
// Update on change
editText.doAfterTextChanged {
val len = it?.length ?: 0
tvCount.text = len.toString()
}
}

View File

@ -1,22 +0,0 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.annotation.DimenRes
import androidx.recyclerview.widget.RecyclerView
class HorizontalMarginItemDecoration(context: Context, @DimenRes horizontalMarginInDp: Int) :
RecyclerView.ItemDecoration() {
private val horizontalMarginInPx: Int =
context.resources.getDimension(horizontalMarginInDp).toInt()
override fun getItemOffsets(
outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
) {
outRect.right = horizontalMarginInPx
outRect.left = horizontalMarginInPx
}
}

View File

@ -0,0 +1,83 @@
package com.alya.ecommerce_serang.utils
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.alya.ecommerce_serang.R
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
object PopUpDialog {
fun showConfirmDialog(
context: Context,
title: String ,
message: String? = null,
iconRes: Int? = null,
positiveText: String? = null,
negativeText: String? = null,
onYesClicked: (() -> Unit)? = null,
onNoClicked: (() -> Unit)? = null
) {
val inflater = LayoutInflater.from(context)
val dialogView = inflater.inflate(R.layout.dialog_popup, null)
val iconView = dialogView.findViewById<ImageView>(R.id.dialogIcon)
val titleView = dialogView.findViewById<TextView>(R.id.dialogTitle)
val messageView = dialogView.findViewById<TextView>(R.id.dialogMessage)
val yesButton = dialogView.findViewById<MaterialButton>(R.id.btnYes)
val noButton = dialogView.findViewById<MaterialButton>(R.id.btnNo)
if (iconRes != null) {
iconView.setImageResource(iconRes)
iconView.visibility = View.VISIBLE
} else {
iconView.visibility = View.GONE
}
// Title
titleView.text = title
// Message
if (message.isNullOrEmpty()) {
messageView.visibility = View.GONE
} else {
messageView.text = message
messageView.visibility = View.VISIBLE
}
// Yes button (always visible, but customizable text)
if (positiveText.isNullOrEmpty()) {
yesButton.visibility = View.GONE
} else {
yesButton.text = positiveText
yesButton.visibility = View.VISIBLE
}
// No button (optional)
if (negativeText.isNullOrEmpty()) {
noButton.visibility = View.GONE
} else {
noButton.text = negativeText
noButton.visibility = View.VISIBLE
}
val dialog = MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_MyApp_AlertDialog)
.setView(dialogView)
.create()
yesButton.setOnClickListener {
onYesClicked?.invoke()
dialog.dismiss()
}
noButton.setOnClickListener {
onNoClicked?.invoke()
dialog.dismiss()
}
dialog.show()
}
}

View File

@ -7,7 +7,6 @@ import android.provider.OpenableColumns
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import kotlin.random.Random
@ -123,23 +122,4 @@ object UriToFileConverter {
null
}
}
fun getFilePathFromUri(uri: Uri, context: Context): String? {
// For Media Gallery
val projection = arrayOf(MediaStore.Images.Media.DATA)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
return it.getString(columnIndex)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting file path from URI", e)
}
// If the above method fails, try direct conversion
return uri.path
}
}

View File

@ -6,10 +6,13 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
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.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.Payment
import com.alya.ecommerce_serang.data.api.response.store.profile.Shipping
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.data.repository.Result
@ -17,18 +20,25 @@ import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.NumberFormat
import java.util.Locale
class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
private var TAG = "MyStoreViewModel"
private val _myStoreProfile = MutableLiveData<Store?>()
val myStoreProfile: LiveData<Store?> = _myStoreProfile
private val _myStoreProfile = MutableLiveData<StoreResponse?>()
val myStoreProfile: LiveData<StoreResponse?> = _myStoreProfile
private val _storeTypes = MutableLiveData<List<StoreTypesItem>>()
val storeTypes: LiveData<List<StoreTypesItem>> = _storeTypes
private val _shipping = MutableLiveData<List<Shipping>>()
val shipping: LiveData<List<Shipping>> = _shipping
private val _payment = MutableLiveData<List<Payment>>()
val payment: LiveData<List<Payment>> = _payment
private val _isLoadingType = MutableLiveData<Boolean>()
val isLoadingType: LiveData<Boolean> = _isLoadingType
@ -47,7 +57,12 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
fun loadMyStore(){
viewModelScope.launch {
when (val result = repository.fetchMyStoreProfile()){
is Result.Success -> _myStoreProfile.postValue(result.data)
is Result.Success -> {
val storeData = result.data
_myStoreProfile.postValue(storeData)
_shipping.postValue(storeData?.shipping)
_payment.postValue(storeData?.payment)
}
is Result.Error -> _errorMessage.postValue(result.exception.message ?: "Unknown Error")
is Result.Loading -> null
}
@ -186,6 +201,80 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
}
}
private fun String.toRequestBody(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
private fun String.toPlain(): RequestBody =
this.toRequestBody("text/plain".toMediaTypeOrNull())
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?
) {
viewModelScope.launch {
try {
val store = myStoreProfile.value
if (store == null) {
_errorMessage.postValue("Data toko tidak tersedia")
Log.e(TAG, "Store data is null")
return@launch
}
val response = repository.updateStoreApproval(
storeName = storeName,
description = description,
storeType = storeType,
latitude = latitude,
longitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
userPhone = userPhone,
paymentsToUpdate = paymentsToUpdate,
paymentIdToDelete = paymentIdToDelete,
storeCourier = storeCourier,
storeImage = storeImage,
ktpImage = ktpImage,
npwpDocument = npwpDocument,
nibDocument = nibDocument
)
if (response != null) {
if (response.isSuccessful) {
_updateStoreProfileResult.postValue(response.body())
Log.d(TAG, "Update successful: ${response.body()}")
} else {
_errorMessage.postValue("Gagal memperbarui profil")
Log.e(TAG, "Update failed: ${response.errorBody()?.string()}")
}
} else {
_errorMessage.postValue("Terjadi kesalahan jaringan atau server")
Log.e(TAG, "Repository returned null response")
}
} catch (e: Exception) {
_errorMessage.postValue(e.message ?: "Unexpected error")
Log.e(TAG, "Exception updating store profile", e)
}
}
}
}

View File

@ -8,6 +8,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile
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.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.repository.Result
@ -27,6 +29,10 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore
private val _deleteFCMT = MutableLiveData<String>()
val deleteFCMT: LiveData<String> = _deleteFCMT
val changePasswordResult = MutableLiveData<Result<ChangePassResponse>>()
private val _logout = MutableLiveData<Boolean>()
val logout : LiveData<Boolean> = _logout
@ -60,7 +66,25 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
}
}
fun deleteFCM(){
viewModelScope.launch {
try {
// Call the repository function to request OTP
val response: DeleteFCMResponse = userRepository.deleteFCMToken()
// Log and store success message
Log.d("ProfileViewModel", "Has store: ${response.message}")
_deleteFCMT.postValue(response.message) // Store the message for UI feedback
} catch (exception: Exception) {
// Handle any errors and update state
_deleteFCMT.postValue(exception.message)
// Log the error for debugging
Log.e(":ProfileViewModel", "Error:", exception)
}
}
}
fun editProfileDirect(
context: Context,
@ -110,6 +134,20 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
}
}
fun changePassword(currentPassword: String, newPassword: String) {
viewModelScope.launch {
try {
// Call the repository to change the password
val result = userRepository.changePassword(currentPassword, newPassword)
// Post the result (success or error) to LiveData
changePasswordResult.postValue(result)
} catch (e: Exception) {
// Handle any unexpected errors
changePasswordResult.postValue(Result.Error(e))
}
}
}
companion object {
private const val TAG = "ProfileViewModel"

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
@ -19,7 +20,8 @@ import com.alya.ecommerce_serang.utils.ImageUtils
import kotlinx.coroutines.launch
class RegisterStoreViewModel(
private val repository: UserRepository
private val repository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// LiveData for UI state
@ -56,20 +58,20 @@ class RegisterStoreViewModel(
var selectedBankName: String? = null
// Form fields
val storeName = MutableLiveData<String>()
val storeDescription = MutableLiveData<String>()
val storeTypeId = MutableLiveData<Int>()
val latitude = MutableLiveData<String>()
val longitude = MutableLiveData<String>()
val street = MutableLiveData<String>()
val subdistrict = MutableLiveData<String>()
val cityId = MutableLiveData<String>()
val provinceId = MutableLiveData<Int>()
val postalCode = MutableLiveData<Int>()
val addressDetail = MutableLiveData<String>()
val bankName = MutableLiveData<String>()
val bankNumber = MutableLiveData<Int>()
val accountName = MutableLiveData<String>()
val storeName: MutableLiveData<String> = savedStateHandle.getLiveData("storeName", "")
val storeDescription: MutableLiveData<String> = savedStateHandle.getLiveData("storeDescription", "")
val storeTypeId: MutableLiveData<Int> = savedStateHandle.getLiveData("storeTypeId", 0)
val latitude: MutableLiveData<String> = savedStateHandle.getLiveData("latitude", "")
val longitude: MutableLiveData<String> = savedStateHandle.getLiveData("longitude", "")
val street: MutableLiveData<String> = savedStateHandle.getLiveData("street", "")
val subdistrict: MutableLiveData<String> = savedStateHandle.getLiveData("subdistrict", "")
val cityId: MutableLiveData<String> = savedStateHandle.getLiveData("cityId", "")
val provinceId: MutableLiveData<Int> = savedStateHandle.getLiveData("provinceId", 0)
val postalCode: MutableLiveData<Int> = savedStateHandle.getLiveData("postalCode", 0)
val addressDetail: MutableLiveData<String> = savedStateHandle.getLiveData("addressDetail", "")
val bankName: MutableLiveData<String> = savedStateHandle.getLiveData("bankName", "")
val bankNumber: MutableLiveData<Int> = savedStateHandle.getLiveData("bankNumber", 0)
val accountName: MutableLiveData<String> = savedStateHandle.getLiveData("accountName", "")
// Files
var storeImageUri: Uri? = null
@ -79,6 +81,15 @@ class RegisterStoreViewModel(
var persetujuanUri: Uri? = null
var qrisUri: Uri? = null
fun getFieldValue(key: String): String {
return savedStateHandle.get<String>(key) ?: ""
}
// Helper function to update any field
fun updateField(key: String, value: String) {
savedStateHandle[key] = value
}
// Selected couriers
val selectedCouriers = mutableListOf<String>()

View File

@ -7,9 +7,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.CreateAddressRequest
import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
import com.alya.ecommerce_serang.data.api.response.auth.RegisterResponse
@ -388,6 +390,34 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
}
fun sendFcm(token: FcmReq) {
viewModelScope.launch {
_otpState.value = Result.Loading // Indicating API call in progress
try {
// Call the repository function to request OTP
val authenticatedApiService = getAuthenticatedApiService()
val authenticatedOrderRepo = UserRepository(authenticatedApiService)
val response: FcmTokenResponse = authenticatedOrderRepo.sendFcm(token)
// Log and store success message
Log.d("LoginViewModel", "OTP Response: ${response.message}")
_message.value = response.message ?: "berhasil" // Store the message for UI feedback
// Update state to indicate success
_otpState.value = Result.Success(Unit)
} catch (exception: Exception) {
// Handle any errors and update state
_otpState.value = Result.Error(exception)
_message.value = exception.localizedMessage ?: "Failed to request OTP"
// Log the error for debugging
Log.e("LoginViewModel", "OTP request failed for: $token", exception)
}
}
}
companion object {
private const val TAG = "RegisterViewModel"
}

View File

@ -17,6 +17,9 @@ import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Locale
@ -52,6 +55,9 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
private val _error = MutableLiveData<String>()
val error: LiveData<String> get() = _error
private val _selectedStatus = MutableStateFlow("all")
val selectedStatus: StateFlow<String> = _selectedStatus.asStateFlow()
fun getSellList(status: String) {
Log.d(TAG, "========== Starting getSellList ==========")
Log.d(TAG, "Requested status: '$status'")
@ -320,4 +326,40 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
Log.d(TAG, "========== refreshOrders method completed ==========")
}
fun updateStatus(status: String, forceRefresh: Boolean = false) {
Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
// Noop guard (optional): skip if user reselects same tab and no refresh asked
if (_selectedStatus.value == status && !forceRefresh) {
Log.d(TAG, "🔸 Status unchanged & forceRefresh = false → skip update")
return
}
_selectedStatus.value = status
Log.d(TAG, "✅ _selectedStatus set to \"$status\"")
if (forceRefresh) {
Log.d(TAG, "🔄 forceRefresh = true → launching refresh()")
viewModelScope.launch { refresh(status) }
}
}
private suspend fun refresh(status: String) {
Log.d(TAG, "⏳ refresh(\"$status\") started")
try {
if (status == "all") {
Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
getAllStatusCounts() // network → cache
} else {
Log.d(TAG, "🌐 repository.getOrderList(\"$status\")")
repository.getSellList(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)
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="100dp" android:tint="#489EC6" android:viewportHeight="24" android:viewportWidth="24" android:width="100dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,6c1.93,0 3.5,1.57 3.5,3.5S13.93,13 12,13s-3.5,-1.57 -3.5,-3.5S10.07,6 12,6zM12,20c-2.03,0 -4.43,-0.82 -6.14,-2.88C7.55,15.8 9.68,15 12,15s4.45,0.8 6.14,2.12C16.43,19.18 14.03,20 12,20z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#489EC6" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2 -0.9,-2 -2,-2zM1,2v2h2l3.6,7.59 -1.35,2.45c-0.16,0.28 -0.25,0.61 -0.25,0.96 0,1.1 0.9,2 2,2h12v-2L7.42,15c-0.14,0 -0.25,-0.11 -0.25,-0.25l0.03,-0.12 0.9,-1.63h7.45c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49c0.08,-0.14 0.12,-0.31 0.12,-0.48 0,-0.55 -0.45,-1 -1,-1L5.21,4l-0.94,-2L1,2zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#211E1E" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM20,18L4,18v-6h16v6zM20,8L4,8L4,6h16v2z"/>
</vector>

View File

@ -3,6 +3,6 @@
android:shape="rectangle">
<solid android:color="@color/blue_500" />
<corners android:radius="5dp" />
<corners android:radius="24dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="95dp" android:viewportHeight="101" android:viewportWidth="101" android:width="95dp">
<path android:fillColor="#BAE0BD" android:pathData="M50.56,97.03C25.06,97.03 4.31,76.28 4.31,50.78C4.31,25.28 25.06,4.53 50.56,4.53C76.06,4.53 96.81,25.28 96.81,50.78C96.81,76.28 76.06,97.03 50.56,97.03Z"/>
<path android:fillColor="#5E9C76" android:pathData="M50.56,5.78C75.31,5.78 95.56,26.03 95.56,50.78C95.56,75.53 75.31,95.78 50.56,95.78C25.81,95.78 5.56,75.53 5.56,50.78C5.56,26.03 25.81,5.78 50.56,5.78ZM50.56,3.28C24.31,3.28 3.06,24.53 3.06,50.78C3.06,77.03 24.31,98.28 50.56,98.28C76.81,98.28 98.06,77.03 98.06,50.78C98.06,24.53 76.81,3.28 50.56,3.28Z"/>
<path android:fillColor="#00000000" android:pathData="M28.56,51.03L43.06,65.53L76.06,32.53" android:strokeColor="#ffffff" android:strokeWidth="7.5"/>
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:pathData="M50,96.88C24.15,96.88 3.13,75.85 3.13,50C3.13,24.15 24.15,3.13 50,3.13C75.85,3.13 96.88,24.15 96.88,50C96.88,75.85 75.85,96.88 50,96.88Z"
android:fillColor="#F78F8F"/>
<path
android:pathData="M50,3.75C75.5,3.75 96.25,24.5 96.25,50C96.25,75.5 75.5,96.25 50,96.25C24.5,96.25 3.75,75.5 3.75,50C3.75,24.5 24.5,3.75 50,3.75ZM50,2.5C23.77,2.5 2.5,23.77 2.5,50C2.5,76.23 23.77,97.5 50,97.5C76.23,97.5 97.5,76.23 97.5,50C97.5,23.77 76.23,2.5 50,2.5Z"
android:fillColor="#C74343"/>
<path
android:pathData="M34.97,70.33L29.67,65.03L65.03,29.67L70.33,34.97L34.97,70.33Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M29.67,34.97L34.97,29.67L70.33,65.03L65.03,70.33L29.67,34.97Z"
android:fillColor="#ffffff"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Background circle for better splash screen appearance -->
<path
android:pathData="M54,54m-50,0a50,50 0,1 1,100 0a50,50 0,1 1,-100 0"
android:fillColor="#ffff"/>
<!-- Main logo content scaled and centered -->
<group android:translateX="32" android:translateY="32" android:scaleX="0.7" android:scaleY="0.7">
<!-- Background rectangle -->
<path
android:pathData="M0,0h64v64h-64z"
android:fillColor="#489EC6"/>
<!-- Main logo shape -->
<path
android:pathData="M11.868,58C10.523,58 9.399,57.538 8.497,56.614C7.594,55.69 7.144,54.537 7.146,53.155V29.074C5.912,28.14 5.009,26.915 4.438,25.399C3.867,23.883 3.854,22.266 4.4,20.548L7.245,10.924C7.635,9.7 8.264,8.74 9.131,8.044C10.001,7.348 11.075,7 12.354,7H51.536C52.813,7 53.883,7.326 54.747,7.978C55.608,8.63 56.24,9.573 56.641,10.807L59.598,20.545C60.146,22.265 60.134,23.896 59.563,25.438C58.992,26.98 58.089,28.23 56.855,29.188V53.152C56.855,54.534 56.404,55.687 55.501,56.611C54.599,57.535 53.476,57.998 52.133,58H11.868ZM38.433,28C40.311,28 41.712,27.46 42.638,26.38C43.564,25.302 43.954,24.188 43.808,23.038L41.866,10H33.465V22.6C33.465,24.074 33.957,25.342 34.939,26.404C35.922,27.468 37.084,28 38.433,28ZM25.278,28C26.849,28 28.119,27.468 29.088,26.404C30.057,25.34 30.541,24.072 30.541,22.6V10H22.138L20.19,23.269C20.071,24.201 20.468,25.222 21.38,26.332C22.292,27.442 23.594,27.998 25.278,28ZM12.263,28C13.551,28 14.647,27.55 15.55,26.65C16.452,25.75 17.014,24.627 17.234,23.281L19.067,10H12.354C11.716,10 11.209,10.144 10.833,10.432C10.457,10.72 10.176,11.153 9.991,11.731L7.292,21.205C6.812,22.781 7.017,24.308 7.906,25.786C8.795,27.264 10.247,28.002 12.263,28ZM51.738,28C53.488,28 54.886,27.3 55.931,25.9C56.978,24.5 57.237,22.935 56.709,21.205L53.864,11.617C53.676,11.039 53.395,10.625 53.019,10.375C52.642,10.125 52.137,10 51.501,10H44.933L46.767,23.281C46.987,24.627 47.549,25.75 48.451,26.65C49.354,27.55 50.449,28 51.738,28Z"
android:fillColor="#ffffff"/>
<!-- Text elements -->
<path
android:pathData="M20.382,39V30.6H23.97C24.554,30.6 25.046,30.692 25.446,30.876C25.846,31.052 26.15,31.304 26.358,31.632C26.574,31.952 26.682,32.332 26.682,32.772C26.682,33.196 26.59,33.552 26.406,33.84C26.222,34.128 25.978,34.348 25.674,34.5C25.378,34.652 25.05,34.744 24.69,34.776L24.882,34.632C25.274,34.648 25.618,34.752 25.914,34.944C26.21,35.136 26.442,35.388 26.61,35.7C26.786,36.004 26.874,36.34 26.874,36.708C26.874,37.164 26.766,37.564 26.55,37.908C26.334,38.252 26.018,38.52 25.602,38.712C25.186,38.904 24.682,39 24.09,39H20.382ZM22.182,37.536H23.79C24.19,37.536 24.498,37.448 24.714,37.272C24.938,37.088 25.05,36.824 25.05,36.48C25.05,36.136 24.934,35.868 24.702,35.676C24.478,35.484 24.166,35.388 23.766,35.388H22.182V37.536ZM22.182,34.056H23.658C24.042,34.056 24.334,33.968 24.534,33.792C24.742,33.616 24.846,33.368 24.846,33.048C24.846,32.728 24.742,32.48 24.534,32.304C24.334,32.12 24.038,32.028 23.646,32.028H22.182V34.056ZM28.163,39V32.952H29.963V39H28.163ZM29.063,32.256C28.743,32.256 28.483,32.164 28.283,31.98C28.083,31.796 27.983,31.564 27.983,31.284C27.983,30.996 28.083,30.76 28.283,30.576C28.483,30.392 28.743,30.3 29.063,30.3C29.391,30.3 29.655,30.392 29.855,30.576C30.063,30.76 30.167,30.996 30.167,31.284C30.167,31.564 30.063,31.796 29.855,31.98C29.655,32.164 29.391,32.256 29.063,32.256ZM34.069,39.144C33.501,39.144 33.009,39.056 32.593,38.88C32.186,38.696 31.861,38.448 31.622,38.136C31.389,37.824 31.257,37.472 31.226,37.08H33.014C33.046,37.216 33.102,37.34 33.181,37.452C33.27,37.556 33.386,37.64 33.529,37.704C33.681,37.76 33.849,37.788 34.034,37.788C34.234,37.788 34.394,37.764 34.514,37.716C34.641,37.66 34.737,37.588 34.801,37.5C34.866,37.412 34.897,37.32 34.897,37.224C34.897,37.072 34.849,36.956 34.754,36.876C34.666,36.796 34.534,36.732 34.357,36.684C34.181,36.628 33.97,36.576 33.722,36.528C33.433,36.464 33.146,36.392 32.857,36.312C32.577,36.224 32.326,36.116 32.102,35.988C31.885,35.86 31.709,35.696 31.573,35.496C31.445,35.288 31.382,35.036 31.382,34.74C31.382,34.38 31.482,34.056 31.681,33.768C31.882,33.472 32.169,33.24 32.546,33.072C32.922,32.896 33.377,32.808 33.914,32.808C34.674,32.808 35.27,32.976 35.701,33.312C36.133,33.648 36.389,34.1 36.47,34.668H34.79C34.742,34.508 34.641,34.388 34.489,34.308C34.338,34.22 34.146,34.176 33.914,34.176C33.65,34.176 33.45,34.22 33.313,34.308C33.178,34.396 33.11,34.512 33.11,34.656C33.11,34.752 33.153,34.84 33.242,34.92C33.338,34.992 33.473,35.056 33.65,35.112C33.826,35.168 34.042,35.224 34.298,35.28C34.785,35.384 35.206,35.496 35.557,35.616C35.917,35.736 36.197,35.912 36.397,36.144C36.597,36.368 36.694,36.696 36.686,37.128C36.694,37.52 36.59,37.868 36.374,38.172C36.166,38.476 35.866,38.716 35.473,38.892C35.082,39.06 34.613,39.144 34.069,39.144ZM40.094,39.144C39.59,39.144 39.17,39.064 38.834,38.904C38.506,38.744 38.262,38.528 38.102,38.256C37.95,37.976 37.874,37.668 37.874,37.332C37.874,36.972 37.962,36.656 38.138,36.384C38.322,36.104 38.606,35.884 38.99,35.724C39.374,35.556 39.858,35.472 40.442,35.472H41.906C41.906,35.2 41.87,34.976 41.798,34.8C41.734,34.624 41.626,34.492 41.474,34.404C41.322,34.316 41.114,34.272 40.85,34.272C40.57,34.272 40.334,34.328 40.142,34.44C39.95,34.552 39.83,34.728 39.782,34.968H38.054C38.094,34.536 38.234,34.16 38.474,33.84C38.722,33.52 39.05,33.268 39.458,33.084C39.866,32.9 40.334,32.808 40.862,32.808C41.438,32.808 41.938,32.904 42.362,33.096C42.786,33.28 43.114,33.552 43.346,33.912C43.586,34.272 43.706,34.72 43.706,35.256V39H42.206L41.99,38.124C41.902,38.276 41.798,38.416 41.678,38.544C41.558,38.664 41.418,38.772 41.258,38.868C41.098,38.956 40.922,39.024 40.73,39.072C40.538,39.12 40.326,39.144 40.094,39.144ZM40.538,37.776C40.73,37.776 40.898,37.744 41.042,37.68C41.186,37.616 41.31,37.528 41.414,37.416C41.518,37.304 41.602,37.176 41.666,37.032C41.738,36.88 41.79,36.716 41.822,36.54V36.528H40.658C40.458,36.528 40.29,36.556 40.154,36.612C40.026,36.66 39.93,36.732 39.866,36.828C39.802,36.924 39.77,37.036 39.77,37.164C39.77,37.3 39.802,37.416 39.866,37.512C39.938,37.6 40.03,37.668 40.142,37.716C40.262,37.756 40.394,37.776 40.538,37.776ZM17.132,53.144C16.5,53.144 15.924,53.02 15.404,52.772C14.892,52.516 14.484,52.136 14.18,51.632C13.884,51.128 13.736,50.492 13.736,49.724V44.6H15.536V49.736C15.536,50.112 15.596,50.432 15.716,50.696C15.844,50.96 16.028,51.16 16.268,51.296C16.516,51.424 16.812,51.488 17.156,51.488C17.508,51.488 17.804,51.424 18.044,51.296C18.292,51.16 18.48,50.96 18.608,50.696C18.736,50.432 18.8,50.112 18.8,49.736V44.6H20.6V49.724C20.6,50.492 20.44,51.128 20.12,51.632C19.808,52.136 19.388,52.516 18.86,52.772C18.34,53.02 17.764,53.144 17.132,53.144ZM22.128,53V44.6H24.288L26.736,49.652L29.16,44.6H31.308V53H29.508V47.636L27.444,51.824H26.004L23.928,47.636V53H22.128ZM32.921,53V44.6H34.721V47.78L37.625,44.6H39.833L36.749,47.924L39.941,53H37.733L35.465,49.316L34.721,50.12V53H32.921ZM41.007,53V44.6H43.167L45.615,49.652L48.039,44.6H50.187V53H48.387V47.636L46.323,51.824H44.883L42.807,47.636V53H41.007Z"
android:fillColor="#489EC6"/>
</group>
</vector>

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