mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-12-16 16:01:02 +00:00
Compare commits
40 Commits
22122d631b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f4d04ac7a | |||
| 593231285a | |||
| d7ffd29032 | |||
| 7c7941d5b2 | |||
| 83c5f2acff | |||
| 971d489939 | |||
| 9b8c92605c | |||
| bc9b16b4af | |||
| 86b5534cb3 | |||
| 7fc6458f9b | |||
| 66595fcb48 | |||
| 57f3c463cb | |||
| 96f6e8c251 | |||
| 3627cdd151 | |||
| 442f9fc10c | |||
| 9273e01324 | |||
| cef4bfa2b2 | |||
| 1eb135d48e | |||
| fe5ecf28e5 | |||
| 3d733b7e0f | |||
| b3d2527ebc | |||
| 16a0a33f11 | |||
| 45fddf6116 | |||
| 1d9399fd4d | |||
| 0887a7e898 | |||
| 29fb55e3c0 | |||
| 2f16542e5e | |||
| 2f28a23114 | |||
| 94c081e839 | |||
| d32bdf65fe | |||
| 9162b2cc60 | |||
| 8eac90311e | |||
| 9cd0675d82 | |||
| f88a5a46ad | |||
| 421c20cc4b | |||
| 792e247eaa | |||
| b6b701fa3b | |||
| 94bd32d6b0 | |||
| 6baf4ee5ce | |||
| ff8654d12a |
19
.idea/deploymentTargetSelector.xml
generated
19
.idea/deploymentTargetSelector.xml
generated
@ -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>
|
||||
|
||||
2
.idea/deviceManager.xml
generated
2
.idea/deviceManager.xml
generated
@ -5,7 +5,7 @@
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
<option name="order" value="DESCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
|
||||
@ -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")
|
||||
|
||||
}
|
||||
|
||||
@ -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 |
@ -0,0 +1,6 @@
|
||||
package com.alya.ecommerce_serang.data.api.dto
|
||||
|
||||
data class ChangePasswordRequest(
|
||||
val currentPassword: String,
|
||||
val newPassword: String
|
||||
)
|
||||
@ -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
|
||||
)
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
)
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
)
|
||||
@ -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
|
||||
@ -81,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
|
||||
@ -97,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")
|
||||
@ -110,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")
|
||||
@ -202,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(
|
||||
@ -254,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(
|
||||
@ -372,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(
|
||||
@ -386,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(
|
||||
@ -402,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")
|
||||
@ -419,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
|
||||
@ -462,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(
|
||||
@ -530,7 +523,12 @@ interface ApiService {
|
||||
@Body request: ResetPassReq
|
||||
): Response<ResetPassResponse>
|
||||
|
||||
@GET("address/detail/{id}")
|
||||
@POST("changepass")
|
||||
suspend fun changePassword(
|
||||
@Body request: ChangePasswordRequest
|
||||
): Response<ChangePassResponse>
|
||||
|
||||
@GET("profile/address/detail/{id}")
|
||||
suspend fun getDetailAddress(
|
||||
@Path("id") addressId: Int
|
||||
): Response<AddressDetailResponse>
|
||||
|
||||
@ -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 you’re 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"
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -41,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)
|
||||
|
||||
@ -118,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 {
|
||||
@ -151,7 +154,7 @@ class CartActivity : AppCompatActivity() {
|
||||
|
||||
// Debug log
|
||||
Log.d(
|
||||
TAG,
|
||||
TAG,
|
||||
"cartItemId: ${updatedCartItem.cartItemId}, " +
|
||||
"isWholesale: ${info.isWholesale}, " +
|
||||
"wholesalePrice: $wholesalePrice, " +
|
||||
@ -164,7 +167,17 @@ class CartActivity : AppCompatActivity() {
|
||||
val cartItemIds = updatedItems.map { it.cartItem.cartItemId }
|
||||
val wholesaleArray = updatedItems.map { it.isWholesale }.toBooleanArray()
|
||||
|
||||
CheckoutActivity.startForCart(this, cartItemIds, wholesaleArray)
|
||||
// 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() {
|
||||
@ -186,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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,6 +253,10 @@ class CartActivity : AppCompatActivity() {
|
||||
storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.productImages.observe(this) { productImages ->
|
||||
storeAdapter.updateProductImages(productImages)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyState(isEmpty: Boolean) {
|
||||
@ -257,5 +275,5 @@ class CartActivity : AppCompatActivity() {
|
||||
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
|
||||
return format.format(amount).replace("Rp", "Rp ")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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", "")
|
||||
|
||||
@ -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 {
|
||||
@ -79,9 +82,6 @@ class CheckoutActivity : AppCompatActivity() {
|
||||
// Determine if this is Buy Now or Cart checkout
|
||||
val isBuyNow = intent.hasExtra(EXTRA_PRODUCT_ID) && !intent.hasExtra(EXTRA_CART_ITEM_IDS)
|
||||
val isWholesaleNow = intent.getBooleanExtra(EXTRA_ISWHOLESALE, false)
|
||||
val wholesalePricesArray = intent.getIntArrayExtra(EXTRA_CART_ITEM_WHOLESALE_PRICES)
|
||||
|
||||
|
||||
|
||||
if (isBuyNow) {
|
||||
// Process Buy Now flow
|
||||
@ -99,8 +99,7 @@ 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()) {
|
||||
// Build map of cartItemId -> isWholesale
|
||||
@ -108,17 +107,18 @@ class CheckoutActivity : AppCompatActivity() {
|
||||
cartItemIds.mapIndexed { index, id ->
|
||||
id to isWholesaleArray[index]
|
||||
}.toMap()
|
||||
|
||||
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
|
||||
// Build wholesalePriceMap
|
||||
val wholesalePriceMap = cartItemIds.mapIndexed { index, id ->
|
||||
id to (wholesalePricesArray?.get(index) ?: 0)
|
||||
}.toMap()
|
||||
// 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,
|
||||
@ -157,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 ->
|
||||
@ -175,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}")
|
||||
}
|
||||
}
|
||||
@ -186,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")
|
||||
|
||||
@ -258,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() {
|
||||
@ -293,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"
|
||||
@ -301,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
|
||||
}
|
||||
}
|
||||
@ -313,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
|
||||
}
|
||||
|
||||
@ -347,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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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(
|
||||
@ -96,7 +99,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
fun initializeFromCart(
|
||||
cartItemIds: List<Int>,
|
||||
isWholesaleMap: Map<Int, Boolean> = emptyMap(),
|
||||
wholesalePriceMap: Map<Int, Int> = emptyMap() // new
|
||||
wholesalePriceMap: Map<Int, Int> = emptyMap()
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
@ -111,13 +114,21 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
for (store in cartResult.data) {
|
||||
val storeItems = store.cartItems.filter { it.cartItemId in cartItemIds }
|
||||
if (storeItems.isNotEmpty()) {
|
||||
// ✅ Override price with wholesale price if exists
|
||||
// ✅ Apply wholesale prices - Replace item prices with wholesale prices
|
||||
val updatedItems = storeItems.map { item ->
|
||||
val wholesalePrice = wholesalePriceMap[item.cartItemId]
|
||||
if (wholesalePrice != null) {
|
||||
item.copy(price = wholesalePrice.toInt())
|
||||
} else item
|
||||
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
|
||||
@ -137,22 +148,29 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
isReseller = isWholesaleMap.any { it.value }
|
||||
)
|
||||
|
||||
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,
|
||||
cartItems = matchingItems, // These now have updated wholesale prices
|
||||
cartItemWholesaleMap = isWholesaleMap
|
||||
)
|
||||
Log.d(TAG, "CheckoutData initialized: ${_checkoutData.value}")
|
||||
|
||||
Log.d(TAG, "Matching cart items: ${matchingItems.size}")
|
||||
Log.d(TAG, "Cart items: ${matchingItems.map { it.productName to it.price }}")
|
||||
Log.d(TAG, "IsWholesaleMap passed: $isWholesaleMap")
|
||||
Log.d(TAG, "WholesalePriceMap passed: $wholesalePriceMap")
|
||||
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 {
|
||||
@ -163,13 +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 {
|
||||
@ -369,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)
|
||||
}
|
||||
|
||||
@ -407,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
|
||||
@ -418,9 +464,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CheckoutViewModel"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
||||
set(value) { savedStateHandle["selectedCityId"] = value }
|
||||
|
||||
var selectedSubdistrict: String? = null
|
||||
var selectedSubdistrictId: String? = null
|
||||
var selectedVillages: String? = null
|
||||
|
||||
init {
|
||||
@ -104,8 +105,6 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun getProvinces() {
|
||||
_provincesState.value = ViewState.Loading
|
||||
viewModelScope.launch {
|
||||
@ -150,18 +149,18 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
|
||||
selectedSubdistrict = cityId
|
||||
selectedSubdistrictId = cityId
|
||||
val result = repository.getListSubdistrict(cityId)
|
||||
result?.let {
|
||||
_subdistrictState.postValue(Result.Success(it.subdistricts))
|
||||
Log.d(TAG, "Cities loaded for province $cityId: ${it.subdistricts.size}")
|
||||
Log.d(TAG, "Subdistrict loaded for city $cityId: ${it.subdistricts.size}")
|
||||
} ?: run {
|
||||
_subdistrictState.postValue(Result.Error(Exception("Failed to load cities")))
|
||||
Log.e(TAG, "City result was null for province $cityId")
|
||||
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 cities for province $cityId", e)
|
||||
Log.e(TAG, "Error fetching subdistrict for city $cityId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -175,14 +174,14 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
||||
val result = repository.getListVillages(subdistrictId)
|
||||
result?.let {
|
||||
_villagesState.postValue(Result.Success(it.villages))
|
||||
Log.d(TAG, "Cities loaded for province $subdistrictId: ${it.villages.size}")
|
||||
Log.d(TAG, "Villages loaded for subdistrict $subdistrictId: ${it.villages.size}")
|
||||
} ?: run {
|
||||
_villagesState.postValue(Result.Error(Exception("Failed to load cities")))
|
||||
Log.e(TAG, "City result was null for province $subdistrictId")
|
||||
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 cities for province $subdistrictId", e)
|
||||
Log.e(TAG, "Error fetching villages for subdistrict $subdistrictId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,10 +247,34 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AddAddressViewModel"
|
||||
}
|
||||
|
||||
@ -52,12 +52,12 @@ class AddressActivity : AppCompatActivity() {
|
||||
windowInsets
|
||||
}
|
||||
|
||||
viewModel.fetchAddresses()
|
||||
|
||||
setupToolbar()
|
||||
setupRecyclerView()
|
||||
setupObservers()
|
||||
|
||||
viewModel.fetchAddresses()
|
||||
}
|
||||
|
||||
|
||||
@ -126,6 +126,11 @@ class AddressActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.fetchAddresses()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ADDRESS_ID = "extra_address_id"
|
||||
}
|
||||
|
||||
@ -371,19 +371,35 @@ class EditAddressActivity : AppCompatActivity() {
|
||||
private fun createNewAddressFromInputs(oldAddress: AddressDetail): AddressDetail {
|
||||
val selectedProvinceId = getSelectedProvinceId()
|
||||
val selectedCityId = getSelectedCityId()
|
||||
val selectedSubdistrictId = getSelectedSubdistrictId()
|
||||
val selectedSubdistrictName = getSelectedSubdistrictName()
|
||||
val selectedVillageId = getSelectedVillageId()
|
||||
val selectedSubdistrictId = getSelectedSubdistrictId()
|
||||
val selectedVillageName = getSelectedVillageName()
|
||||
val selectedProvinceName = getSelectedProvinceName()
|
||||
|
||||
return oldAddress.copy(
|
||||
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(),
|
||||
cityId = selectedCityId.toString(),
|
||||
subdistrict = selectedSubdistrictId.toString(),
|
||||
villageId = selectedVillageId
|
||||
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? {
|
||||
@ -392,12 +408,30 @@ class EditAddressActivity : AppCompatActivity() {
|
||||
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 }
|
||||
@ -410,6 +444,12 @@ class EditAddressActivity : AppCompatActivity() {
|
||||
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"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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 re‑query */
|
||||
historyVm.updateStatus(status, forceRefresh = true)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
statusPage()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 can’t 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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 you’re 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"
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() ?: "-"
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 adapter’s 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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>()
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
|
||||
// No‑op guard (optional): skip if user re‑selects 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 |
@ -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>
|
||||
5
app/src/main/res/drawable/baseline_circle_24.xml
Normal file
5
app/src/main/res/drawable/baseline_circle_24.xml
Normal 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>
|
||||
@ -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>
|
||||
5
app/src/main/res/drawable/baseline_payment_24.xml
Normal file
5
app/src/main/res/drawable/baseline_payment_24.xml
Normal 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>
|
||||
@ -3,6 +3,6 @@
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@color/blue_500" />
|
||||
<corners android:radius="5dp" />
|
||||
<corners android:radius="24dp" />
|
||||
|
||||
</shape>
|
||||
|
||||
9
app/src/main/res/drawable/checkmark__1_.xml
Normal file
9
app/src/main/res/drawable/checkmark__1_.xml
Normal 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>
|
||||
18
app/src/main/res/drawable/ic_cancel.xml
Normal file
18
app/src/main/res/drawable/ic_cancel.xml
Normal 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>
|
||||
BIN
app/src/main/res/drawable/ic_change_pass.png
Normal file
BIN
app/src/main/res/drawable/ic_change_pass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
29
app/src/main/res/drawable/logo_bisa_umkm.xml
Normal file
29
app/src/main/res/drawable/logo_bisa_umkm.xml
Normal 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>
|
||||
10
app/src/main/res/drawable/logo_home.xml
Normal file
10
app/src/main/res/drawable/logo_home.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/white" />
|
||||
<!-- Logo with controlled size -->
|
||||
<item
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:drawable="@drawable/logo_psb_only"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
BIN
app/src/main/res/drawable/logo_psb_crop.jpeg
Normal file
BIN
app/src/main/res/drawable/logo_psb_crop.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user