Compare commits

18 Commits

105 changed files with 2584 additions and 772 deletions

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import java.util.Properties import java.util.Properties
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
@ -127,4 +128,8 @@ dependencies {
//Splash screen //Splash screen
implementation("androidx.core:core-splashscreen:1.0.0") implementation("androidx.core:core-splashscreen:1.0.0")
//pdf compression
implementation("com.tom-roush:pdfbox-android:2.0.27.0")
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

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

View File

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

View File

@ -62,6 +62,9 @@ data class Product(
@field:SerializedName("wholesale_min_item") @field:SerializedName("wholesale_min_item")
val wholesaleMinItem: Int? = null, val wholesaleMinItem: Int? = null,
@field:SerializedName("status")
val status: String,
@field:SerializedName("min_order") @field:SerializedName("min_order")
val minOrder: Int, val minOrder: Int,

View File

@ -1,7 +1,6 @@
package com.alya.ecommerce_serang.data.api.retrofit package com.alya.ecommerce_serang.data.api.retrofit
import com.alya.ecommerce_serang.data.api.dto.AddEvidenceRequest
import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse import com.alya.ecommerce_serang.data.api.dto.AddPaymentInfoResponse
import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq import com.alya.ecommerce_serang.data.api.dto.CancelOrderReq
import com.alya.ecommerce_serang.data.api.dto.CartItem import com.alya.ecommerce_serang.data.api.dto.CartItem
@ -17,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.OrderRequest
import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy import com.alya.ecommerce_serang.data.api.dto.OrderRequestBuy
import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.PaymentConfirmRequest
import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse import com.alya.ecommerce_serang.data.api.dto.ProvinceResponse
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.ResetPassReq import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
@ -29,7 +27,7 @@ import com.alya.ecommerce_serang.data.api.dto.UpdateCart
import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest import com.alya.ecommerce_serang.data.api.dto.UpdateChatRequest
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse
import com.alya.ecommerce_serang.data.api.response.auth.CheckStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.DeleteFCMResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse import com.alya.ecommerce_serang.data.api.response.auth.ListNotifResponse
@ -83,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.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@ -99,7 +95,6 @@ import retrofit2.http.PUT
import retrofit2.http.Part import retrofit2.http.Part
import retrofit2.http.PartMap import retrofit2.http.PartMap
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
interface ApiService { interface ApiService {
@POST("registeruser") @POST("registeruser")
@ -112,8 +107,9 @@ interface ApiService {
@Body verifRegisReq: VerifRegisReq @Body verifRegisReq: VerifRegisReq
):VerifRegisterResponse ):VerifRegisterResponse
@GET("checkstore") @PUT("deletefcm")
suspend fun checkStore (): Response<CheckStoreResponse> suspend fun deleteFCMToken (
): DeleteFCMResponse
@Multipart @Multipart
@POST("registerstore") @POST("registerstore")
@ -204,11 +200,6 @@ interface ApiService {
@Path("id") orderId: Int @Path("id") orderId: Int
): Response<OrderDetailResponse> ): Response<OrderDetailResponse>
@POST("order/addevidence")
suspend fun addEvidence(
@Body request : AddEvidenceRequest,
): Response<AddEvidenceResponse>
@Multipart @Multipart
@POST("order/addevidence") @POST("order/addevidence")
suspend fun addEvidenceMultipart( suspend fun addEvidenceMultipart(
@ -256,15 +247,9 @@ interface ApiService {
@GET("mystore") @GET("mystore")
suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse> suspend fun getMyStoreData(): Response<com.alya.ecommerce_serang.data.api.response.store.StoreResponse>
@GET("mystore")
suspend fun getStoreAddress(): Response<StoreAddressResponse>
@GET("mystore/product") // Replace with actual endpoint @GET("mystore/product") // Replace with actual endpoint
suspend fun getStoreProduct(): Response<ViewStoreProductsResponse> suspend fun getStoreProduct(): Response<ViewStoreProductsResponse>
@GET("category")
fun getCategories(): Call<CategoryResponse>
@Multipart @Multipart
@POST("store/createproduct") @POST("store/createproduct")
suspend fun addProduct( suspend fun addProduct(
@ -374,9 +359,6 @@ interface ApiService {
@GET("store/topup") @GET("store/topup")
suspend fun getTopUpHistory(): Response<TopUpResponse> suspend fun getTopUpHistory(): Response<TopUpResponse>
@GET("store/topup")
suspend fun getFilteredTopUpHistory(@Query("date") date: String): Response<TopUpResponse>
@Multipart @Multipart
@POST("store/createtopup") @POST("store/createtopup")
suspend fun addBalanceTopUp( suspend fun addBalanceTopUp(
@ -388,11 +370,6 @@ interface ApiService {
@Part("bank_num") bankNum: RequestBody @Part("bank_num") bankNum: RequestBody
): Response<BalanceTopUpResponse> ): Response<BalanceTopUpResponse>
@PUT("store/payment/update")
suspend fun paymentConfirmation(
@Body confirmPaymentReq : PaymentConfirmRequest
): Response<PaymentConfirmationResponse>
@Multipart @Multipart
@PUT("mystore/edit") @PUT("mystore/edit")
suspend fun updateStoreProfileMultipart( suspend fun updateStoreProfileMultipart(
@ -404,13 +381,26 @@ interface ApiService {
): Response<StoreDataResponse> ): Response<StoreDataResponse>
@Multipart @Multipart
@POST("mystore/payment/add") @PUT("mystore/edit")
suspend fun addPaymentInfo( suspend fun updateStoreApprovalMultipart(
@Part("bank_name") bankName: RequestBody, @Part("store_name") storeName: RequestBody,
@Part("bank_num") bankNum: RequestBody, @Part("store_description") storeDescription: RequestBody,
@Part("account_name") accountName: RequestBody, @Part("store_type_id") storeTypeId: RequestBody,
@Part qris: MultipartBody.Part? @Part("latitude") storeLatitude: RequestBody,
): Response<GenericResponse> @Part("longitude") storeLongitude: RequestBody,
@Part("province_id") storeProvince: RequestBody,
@Part("city_id") storeCity: RequestBody,
@Part("subdistrict") storeSubdistrict: RequestBody,
@Part("village_id") storeVillage: RequestBody,
@Part("street") storeStreet: RequestBody,
@Part("postal_code") storePostalCode: RequestBody,
@Part("detail") storeAddressDetail: RequestBody,
@Part("user_phone") storeUserPhone: RequestBody,
@Part storeimg: MultipartBody.Part?,
@Part ktp: MultipartBody.Part?,
@Part npwp: MultipartBody.Part?,
@Part nib: MultipartBody.Part?
): Response<StoreDataResponse>
@Multipart @Multipart
@POST("mystore/payment/add") @POST("mystore/payment/add")
@ -421,6 +411,16 @@ interface ApiService {
@Part qris: MultipartBody.Part? @Part qris: MultipartBody.Part?
): Response<AddPaymentInfoResponse> ): Response<AddPaymentInfoResponse>
@Multipart
@PUT("mystore/payment/edit")
suspend fun updatePaymentInfo(
@Part("payment_info_id") paymentInfoId: RequestBody,
@Part("account_name") accountName: RequestBody,
@Part("bank_name") bankName: RequestBody,
@Part("bank_num") bankNum: RequestBody,
@Part qris: MultipartBody.Part? = null
): Response<GenericResponse>
@DELETE("mystore/payment/delete/{id}") @DELETE("mystore/payment/delete/{id}")
suspend fun deletePaymentInfo( suspend fun deletePaymentInfo(
@Path("id") paymentMethodId: Int @Path("id") paymentMethodId: Int
@ -464,15 +464,6 @@ interface ApiService {
@GET("search") @GET("search")
suspend fun getSearchHistory(): Response<SearchHistoryResponse> suspend fun getSearchHistory(): Response<SearchHistoryResponse>
@Multipart
@POST("sendchat")
suspend fun sendChatLine(
@Part("store_id") storeId: RequestBody,
@Part("message") message: RequestBody,
@Part("product_id") productId: RequestBody?,
@Part chatimg: MultipartBody.Part?
): Response<SendChatResponse>
@Multipart @Multipart
@POST("store/sendchat") @POST("store/sendchat")
suspend fun sendChatMessageStore( suspend fun sendChatMessageStore(

View File

@ -1,17 +1,23 @@
package com.alya.ecommerce_serang.data.repository package com.alya.ecommerce_serang.data.repository
import android.util.Log import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.ShippingServiceRequest
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.HttpException import retrofit2.HttpException
import retrofit2.Response import retrofit2.Response
import java.io.File
import java.io.IOException import java.io.IOException
class MyStoreRepository(private val apiService: ApiService) { class MyStoreRepository(private val apiService: ApiService) {
@ -139,6 +145,186 @@ class MyStoreRepository(private val apiService: ApiService) {
} }
} }
suspend fun updateStoreApproval(
storeName: RequestBody,
description: RequestBody,
storeType: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
storeProvince: RequestBody,
storeCity: RequestBody,
storeSubdistrict: RequestBody,
storeVillage: RequestBody,
storeStreet: RequestBody,
storePostalCode: RequestBody,
storeAddressDetail: RequestBody,
userPhone: RequestBody,
paymentsToUpdate: List<PaymentUpdate> = emptyList(),
paymentIdToDelete: List<Int> = emptyList(),
storeCourier: List<String>? = null,
storeImage: MultipartBody.Part?,
ktpImage: MultipartBody.Part?,
npwpDocument: MultipartBody.Part?,
nibDocument: MultipartBody.Part?
): Response<StoreDataResponse>? {
return try {
Log.d(TAG, "Updating store profile & address for approval...")
val profileResp = apiService.updateStoreApprovalMultipart(
storeName = storeName,
storeDescription = description,
storeTypeId = storeType,
storeLatitude = latitude,
storeLongitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
storeUserPhone = userPhone,
storeimg = storeImage,
ktp = ktpImage,
npwp = npwpDocument,
nib = nibDocument
)
if (!profileResp.isSuccessful) {
Log.e(TAG, "Profile update failed: ${profileResp.code()} ${profileResp.errorBody()?.string()}")
return profileResp // short-circuit; let caller inspect the failure
}
// 2) Payments: delete, then upsert (safer if youre changing accounts)
if (paymentIdToDelete.isNotEmpty() || paymentsToUpdate.isNotEmpty()) {
Log.d(TAG, "Synchronizing payments: delete=${paymentIdToDelete.size}, upsert=${paymentsToUpdate.size}")
}
// 2a) Delete payments
paymentIdToDelete.forEach { id ->
runCatching {
apiService.deletePaymentInfo(id)
}.onSuccess {
if (!it.isSuccessful) {
Log.e(TAG, "Delete payment $id failed: ${it.code()} ${it.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted payment $id")
}
}.onFailure { e ->
Log.e(TAG, "Delete payment $id exception", e)
}
}
// 2b) Upsert payments (add if id==null, else update)
paymentsToUpdate.forEach { item ->
runCatching {
// --- CHANGE HERE if your PaymentUpdate field names differ ---
val id = item.id // Int? (null => add)
val bankName = item.bankName // String
val bankNum = item.bankNum // String
val accountName = item.accountName // String
val qrisImage = item.qrisImage // File? (Optional)
// -----------------------------------------------------------
if (id == null) {
// ADD
val resp = apiService.addPaymentInfoDirect(
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
accountName = accountName.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Add payment failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added payment: $bankName/$bankNum")
}
} else {
// UPDATE
val resp = apiService.updatePaymentInfo(
paymentInfoId = id.toString().toPlain(),
accountName = accountName.toPlain(),
bankName = bankName.toPlain(),
bankNum = bankNum.toPlain(),
qris = createQrisPartOrNull(qrisImage)
)
if (!resp.isSuccessful) {
Log.e(TAG, "Update payment $id failed: ${resp.code()} ${resp.errorBody()?.string()}")
} else {
Log.d(TAG, "Updated payment $id: $bankName/$bankNum")
}
}
}.onFailure { e ->
Log.e(TAG, "Upsert payment exception", e)
}
}
// 3) Shipping: sync to desiredCouriers (if provided)
storeCourier?.let { desired ->
try {
val current = apiService.getStoreData().let { resp ->
if (resp.isSuccessful) {
resp.body()?.shipping?.mapNotNull { it.courier } ?: emptyList()
} else {
Log.e(TAG, "Failed to read current shipping: ${resp.code()} ${resp.errorBody()?.string()}")
emptyList()
}
}
val desiredSet = desired.toSet()
val currentSet = current.toSet()
val toAdd = (desiredSet - currentSet).toList()
val toDel = (currentSet - desiredSet).toList()
if (toAdd.isNotEmpty()) {
val addResp = apiService.addShippingService(ShippingServiceRequest(couriers = toAdd))
if (!addResp.isSuccessful) {
Log.e(TAG, "Add couriers failed: ${addResp.code()} ${addResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Added couriers: $toAdd")
}
}
if (toDel.isNotEmpty()) {
val delResp = apiService.deleteShippingService(ShippingServiceRequest(couriers = toDel))
if (!delResp.isSuccessful) {
Log.e(TAG, "Delete couriers failed: ${delResp.code()} ${delResp.errorBody()?.string()}")
} else {
Log.d(TAG, "Deleted couriers: $toDel")
}
}
} catch (e: Exception) {
Log.e(TAG, "Sync shipping exception", e)
}
}
// Return the profile response (already successful here)
profileResp
} catch (e: Exception) {
Log.e(TAG, "Error updating store approval flow", e)
null
}
}
private fun String.toPlain(): RequestBody =
this.toRequestBody("text/plain".toMediaTypeOrNull())
private fun createQrisPartOrNull(file: File?): MultipartBody.Part? =
file?.let {
val mime = when (it.extension.lowercase()) {
"jpg", "jpeg" -> "image/jpeg"
"png" -> "image/png"
else -> "application/octet-stream"
}.toMediaTypeOrNull()
MultipartBody.Part.createFormData(
"qris",
it.name,
it.asRequestBody(mime)
)
}
companion object { companion object {
private var TAG = "MyStoreRepository" private var TAG = "MyStoreRepository"
} }

View File

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

View File

@ -12,6 +12,7 @@ import com.alya.ecommerce_serang.data.api.dto.ResetPassReq
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq import com.alya.ecommerce_serang.data.api.dto.VerifRegisReq
import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse
import com.alya.ecommerce_serang.data.api.response.auth.DeleteFCMResponse
import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse import com.alya.ecommerce_serang.data.api.response.auth.FcmTokenResponse
import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse import com.alya.ecommerce_serang.data.api.response.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
@ -541,6 +542,10 @@ class UserRepository(private val apiService: ApiService) {
} }
} }
suspend fun deleteFCMToken(): DeleteFCMResponse{
return apiService.deleteFCMToken()
}
companion object{ companion object{
private const val TAG = "UserRepository" private const val TAG = "UserRepository"
} }

View File

@ -5,10 +5,9 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.FcmReq import com.alya.ecommerce_serang.data.api.dto.FcmReq
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
@ -16,6 +15,7 @@ import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityLoginBinding import com.alya.ecommerce_serang.databinding.ActivityLoginBinding
import com.alya.ecommerce_serang.ui.MainActivity import com.alya.ecommerce_serang.ui.MainActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
@ -39,9 +39,9 @@ class LoginActivity : AppCompatActivity() {
binding = ActivityLoginBinding.inflate(layoutInflater) binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
//
WindowCompat.setDecorFitsSystemWindows(window, false) // WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge() // enableEdgeToEdge()
setupListeners() setupListeners()
observeLoginState() observeLoginState()
@ -83,6 +83,11 @@ class LoginActivity : AppCompatActivity() {
retrieveFCMToken() retrieveFCMToken()
// sessionManager.saveUserId(response.userId) // sessionManager.saveUserId(response.userId)
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.checkmark__1_,
title = "Berhasil Masuk"
)
Toast.makeText(this, "Berhasil masuk", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Berhasil masuk", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java)) startActivity(Intent(this, MainActivity::class.java))
@ -90,6 +95,11 @@ class LoginActivity : AppCompatActivity() {
} }
is com.alya.ecommerce_serang.data.repository.Result.Error -> { is com.alya.ecommerce_serang.data.repository.Result.Error -> {
Log.e("LoginActivity", "Login Failed: ${result.exception.message}") Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
PopUpDialog.showConfirmDialog(
context = this,
iconRes = R.drawable.ic_cancel,
title = "Gagal Masuk"
)
Toast.makeText(this, "Gagal masuk", Toast.LENGTH_LONG).show() Toast.makeText(this, "Gagal masuk", Toast.LENGTH_LONG).show()
} }
is Result.Loading -> { is Result.Loading -> {

View File

@ -5,13 +5,14 @@ import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity 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.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityResetPassBinding import com.alya.ecommerce_serang.databinding.ActivityResetPassBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel import com.alya.ecommerce_serang.utils.viewmodel.LoginViewModel
class ResetPassActivity : AppCompatActivity() { class ResetPassActivity : AppCompatActivity() {
@ -36,7 +37,7 @@ class ResetPassActivity : AppCompatActivity() {
setupToolbar() setupToolbar()
setupUI() setupUI()
observeResetPassword()
} }
private fun setupToolbar(){ private fun setupToolbar(){
@ -98,26 +99,23 @@ class ResetPassActivity : AppCompatActivity() {
private fun handleSuccess(message: String) { private fun handleSuccess(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
// Show success dialog and navigate back to login PopUpDialog.showConfirmDialog(
AlertDialog.Builder(this) context = this,
.setTitle("Berhasil Ubah Password") iconRes = R.drawable.checkmark__1_,
.setMessage(message) title = "Berhasil Ubah Password",
.setPositiveButton("OK") { _, _ -> positiveText = "OK"
// Navigate back to login activity )
finish()
}
.setCancelable(false)
.show()
} }
private fun handleError(errorMessage: String) { private fun handleError(errorMessage: String) {
Log.e(TAG, "Error: $errorMessage") Log.e(TAG, "Error: $errorMessage")
// Optionally show error dialog PopUpDialog.showConfirmDialog(
AlertDialog.Builder(this) context = this,
.setTitle("Gagal Ubah Password") iconRes = R.drawable.ic_cancel,
.setMessage(errorMessage) title = "Gagal Ubah Password",
.setPositiveButton("OK", null) message = errorMessage,
.show() positiveText = "OK"
)
} }
} }

View File

@ -27,6 +27,7 @@ import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
import com.alya.ecommerce_serang.ui.order.address.ViewState import com.alya.ecommerce_serang.ui.order.address.ViewState
import com.alya.ecommerce_serang.ui.order.address.VillagesAdapter import com.alya.ecommerce_serang.ui.order.address.VillagesAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
@ -108,8 +109,17 @@ class RegisterStep3Fragment : Fragment() {
} }
binding.btnRegister.setOnClickListener { binding.btnRegister.setOnClickListener {
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() submitAddress()
} }
)
}
// Observe address submission state // Observe address submission state
observeAddressSubmissionState() observeAddressSubmissionState()
@ -498,6 +508,7 @@ class RegisterStep3Fragment : Fragment() {
private fun showRegistrationSuccess() { private fun showRegistrationSuccess() {
// Now we can show the success message for the overall registration process // Now we can show the success message for the overall registration process
Toast.makeText(requireContext(), "Berhasil mendaftarkan akun", Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), "Berhasil mendaftarkan akun", Toast.LENGTH_LONG).show()
sessionManager.clearAll() sessionManager.clearAll()

View File

@ -43,8 +43,6 @@ class CartActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager) apiService = ApiConfig.getApiService(sessionManager)
binding = ActivityCartBinding.inflate(layoutInflater) binding = ActivityCartBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -201,7 +199,8 @@ class CartActivity : AppCompatActivity() {
viewModel.errorMessage.observe(this) { errorMessage -> viewModel.errorMessage.observe(this) { errorMessage ->
errorMessage?.let { errorMessage?.let {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show() binding.emptyCart.visibility = View.VISIBLE
Log.e("CartActivity", "Error message: $it")
} }
} }
@ -254,6 +253,10 @@ class CartActivity : AppCompatActivity() {
storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap) storeAdapter.updateWholesaleStatus(wholesaleStatusMap, wholesalePriceMap)
} }
} }
viewModel.productImages.observe(this) { productImages ->
storeAdapter.updateProductImages(productImages)
}
} }
private fun showEmptyState(isEmpty: Boolean) { private fun showEmptyState(isEmpty: Boolean) {
@ -272,5 +275,5 @@ class CartActivity : AppCompatActivity() {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(amount).replace("Rp", "Rp ") return format.format(amount).replace("Rp", "Rp ")
} }
}
}

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding import com.alya.ecommerce_serang.databinding.ActivityCheckoutBinding
import com.alya.ecommerce_serang.ui.order.address.AddressActivity import com.alya.ecommerce_serang.ui.order.address.AddressActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -35,6 +36,8 @@ class CheckoutActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheckoutBinding private lateinit var binding: ActivityCheckoutBinding
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
private var paymentAdapter: PaymentMethodAdapter? = null private var paymentAdapter: PaymentMethodAdapter? = null
private var cartCheckoutAdapter: CartCheckoutAdapter? = null
private var checkoutSellerAdapter: CheckoutSellerAdapter? = null
private var paymentMethodsLoaded = false private var paymentMethodsLoaded = false
private val viewModel: CheckoutViewModel by viewModels { private val viewModel: CheckoutViewModel by viewModels {
@ -209,6 +212,13 @@ class CheckoutActivity : AppCompatActivity() {
finish() finish()
} }
} }
viewModel.productImages.observe(this) { images ->
Log.d("CheckoutActivity", "Product images updated: ${images.keys}")
// Update adapter when images arrive
cartCheckoutAdapter?.updateProductImages(images)
checkoutSellerAdapter?.updateProductImages(images)
}
} }
private fun setupPaymentMethodsRecyclerView(paymentMethods: List<DetailPaymentItem>) { private fun setupPaymentMethodsRecyclerView(paymentMethods: List<DetailPaymentItem>) {
@ -270,11 +280,12 @@ class CheckoutActivity : AppCompatActivity() {
} }
private fun setupProductRecyclerView(checkoutData: CheckoutData) { private fun setupProductRecyclerView(checkoutData: CheckoutData) {
val adapter = if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) { if (checkoutData.isBuyNow || checkoutData.cartItems.size <= 1) {
CheckoutSellerAdapter(checkoutData) Log.d("CheckoutActivity", "Using CheckoutSellerAdapter")
} else { val adapter = CheckoutSellerAdapter(checkoutData)
CartCheckoutAdapter(checkoutData)
} // Keep reference for image updates - create a field in your activity
checkoutSellerAdapter = adapter
binding.rvProductItems.apply { binding.rvProductItems.apply {
layoutManager = LinearLayoutManager(this@CheckoutActivity) layoutManager = LinearLayoutManager(this@CheckoutActivity)
@ -282,12 +293,31 @@ class CheckoutActivity : AppCompatActivity() {
isNestedScrollingEnabled = false isNestedScrollingEnabled = false
} }
// if (checkoutData.cartItems.isEmpty()) { // Load images for cart items
// // Show empty products state if (!checkoutData.isBuyNow) {
// binding.containerEmptyProducts.visibility = View.VISIBLE checkoutData.cartItems.forEach { item ->
// binding.rvProductItems.visibility = View.GONE viewModel.loadProductImage(item.productId)
// return }
// } }
} else {
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.containerEmptyProducts.visibility = View.GONE binding.containerEmptyProducts.visibility = View.GONE
binding.rvProductItems.visibility = View.VISIBLE binding.rvProductItems.visibility = View.VISIBLE
@ -372,8 +402,17 @@ class CheckoutActivity : AppCompatActivity() {
// Create order button // Create order button
binding.btnPay.setOnClickListener { binding.btnPay.setOnClickListener {
if (validateOrder()) { if (validateOrder()) {
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah anda yakin membuat pesanan?",
message = "Pastikan data yang dimasukkan sudah benar",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.createOrder() viewModel.createOrder()
} }
)
}
} }
// // Voucher section (if implemented) // // Voucher section (if implemented)

View File

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

View File

@ -40,7 +40,10 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private val _orderCreated = MutableLiveData<Boolean>() private val _orderCreated = MutableLiveData<Boolean>()
val orderCreated: LiveData<Boolean> = _orderCreated val orderCreated: LiveData<Boolean> = _orderCreated
private val _productImages = MutableLiveData<Map<Int, String>>(emptyMap())
val productImages: LiveData<Map<Int, String>> = _productImages
private val currentImages = mutableMapOf<Int, String>()
// Initialize "Buy Now" checkout // Initialize "Buy Now" checkout
fun initializeBuyNow( fun initializeBuyNow(
@ -158,9 +161,13 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
) )
Log.d(TAG, "CheckoutData initialized with ${matchingItems.size} items") Log.d(TAG, "CheckoutData initialized with ${matchingItems.size} items")
matchingItems.forEachIndexed { index, item -> matchingItems.forEach { item ->
val isWholesale = isWholesaleMap[item.cartItemId] ?: false Log.d("CheckoutViewModel", "About to load image for productId: ${item.productId}")
Log.d(TAG, "Item $index: ${item.productName}, Price: ${item.price}, IsWholesale: $isWholesale") loadProductImage(item.productId)
}
matchingItems.forEach { item ->
loadProductImage(item.productId)
} }
// Calculate totals with updated prices // Calculate totals with updated prices
@ -181,6 +188,8 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
fun getPaymentMethods() { fun getPaymentMethods() {
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -419,6 +428,31 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
fun loadProductImage(productId: Int) {
Log.d("CheckoutViewModel", "loadProductImage called for productId: $productId")
viewModelScope.launch {
try {
Log.d("CheckoutViewModel", "Fetching product detail for productId: $productId")
val productDetail = repository.fetchProductDetail(productId)
Log.d("CheckoutViewModel", "Product detail result: $productDetail")
val imageUrl = productDetail?.product?.image
Log.d("CheckoutViewModel", "Extracted image URL: $imageUrl")
currentImages[productId] = imageUrl.toString()
Log.d("CheckoutViewModel", "Updated currentImages: $currentImages")
_productImages.postValue(currentImages.toMap())
Log.d("CheckoutViewModel", "Posted to _productImages LiveData")
} catch (e: Exception) {
Log.e("CheckoutViewModel", "Error loading image for productId $productId", e)
// fallback if error
currentImages[productId] = ""
_productImages.postValue(currentImages.toMap())
}
}
}
// Get shipping price // Get shipping price
private fun getShippingPrice(): Double { private fun getShippingPrice(): Double {
val data = _checkoutData.value ?: return 0.0 val data = _checkoutData.value ?: return 0.0
@ -434,3 +468,4 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
private const val TAG = "CheckoutViewModel" private const val TAG = "CheckoutViewModel"
} }
} }

View File

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

View File

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

View File

@ -34,6 +34,7 @@ import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding import com.alya.ecommerce_serang.databinding.ActivityAddAddressBinding
import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory import com.alya.ecommerce_serang.utils.SavedStateViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
class AddAddressActivity : AppCompatActivity() { class AddAddressActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAddressBinding private lateinit var binding: ActivityAddAddressBinding
@ -65,6 +66,12 @@ class AddAddressActivity : AppCompatActivity() {
binding = ActivityAddAddressBinding.inflate(layoutInflater) binding = ActivityAddAddressBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
applyLiveCounter(
binding.etDetailAlamat,
binding.tvCountDetail,
binding.tvCountDetailMax
)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager) apiService = ApiConfig.getApiService(sessionManager)
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager locationManager = getSystemService(LOCATION_SERVICE) as LocationManager

View File

@ -33,6 +33,7 @@ import com.alya.ecommerce_serang.data.repository.OrderRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding import com.alya.ecommerce_serang.databinding.ActivityAddEvidencePaymentBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -166,6 +167,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// Submit button // Submit button
binding.btnSubmit.setOnClickListener { binding.btnSubmit.setOnClickListener {
validateAndUpload() validateAndUpload()
Log.d(TAG, "AddEvidencePaymentActivity onCreate completed") Log.d(TAG, "AddEvidencePaymentActivity onCreate completed")
} }
@ -220,8 +222,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
val adapter = object : ArrayAdapter<String>(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) { val adapter = object : ArrayAdapter<String>(this, R.layout.item_dialog_add_evidence, R.id.tvOption, options) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent) val view = super.getView(position, convertView, parent)
val divider = view.findViewById<View>(R.id.divider)
divider.visibility = if (position == count - 1) View.GONE else View.VISIBLE
return view return view
} }
} }
@ -313,12 +313,14 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
return return
} }
//in case applied metode pembayaran yang lain
// if (binding.spinnerPaymentMethod.selectedItemPosition == 0) { // if (binding.spinnerPaymentMethod.selectedItemPosition == 0) {
// Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
// return // return
// } // }
binding.etAccountNumber.visibility = View.GONE binding.etAccountNumber.visibility = View.GONE
//in case applied nomor rekening
// if (binding.etAccountNumber.text.toString().trim().isEmpty()) { // if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
// Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
// return // return
@ -330,8 +332,16 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
// } // }
// All validations passed, proceed with upload // All validations passed, proceed with upload
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah bukti yang dikirimkan sudah benar?",
message = "Pastikan bukti yang dikirimkan valid",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
uploadPaymentProof() uploadPaymentProof()
}
)
} }
private fun uploadPaymentProof() { private fun uploadPaymentProof() {
@ -443,9 +453,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
).show() ).show()
} }
companion object { companion object {
private const val PERMISSION_REQUEST_CODE = 100 private const val PERMISSION_REQUEST_CODE = 100
private const val TAG = "AddEvidenceActivity" private const val TAG = "AddEvidenceActivity"

View File

@ -232,7 +232,6 @@ class PaymentActivity : AppCompatActivity() {
else -> emptyList() else -> emptyList()
} }
// Tampilkan instruksi dalam dialog
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setTitle("Petunjuk Transfer $type") .setTitle("Petunjuk Transfer $type")
.setItems(instructions.toTypedArray(), null) .setItems(instructions.toTypedArray(), null)

View File

@ -57,6 +57,14 @@ class HistoryActivity : AppCompatActivity() {
} }
} }
// override fun onDialogConfirmed() {
// // Option 1: refresh activity
// recreate()
//
// // Or Option 2: reload only data
// // viewModel.loadOrders()
// }
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)

View File

@ -231,10 +231,11 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
} }
} }
private suspend fun refresh(status: String) { fun refresh(status: String) {
Log.d(TAG, "⏳ refresh(\"$status\") started") Log.d(TAG, "⏳ refresh(\"$status\") started")
try { try {
viewModelScope.launch {
if (status == "all") { if (status == "all") {
Log.d(TAG, "🌐 Calling getAllOrdersCombined()") Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
getAllOrdersCombined() // network → cache getAllOrdersCombined() // network → cache
@ -244,6 +245,7 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
} }
Log.d(TAG, "✅ refresh(\"$status\") completed (repository updated)") Log.d(TAG, "✅ refresh(\"$status\") completed (repository updated)")
// Flow that watches DB/cache will emit automatically // Flow that watches DB/cache will emit automatically
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ refresh(\"$status\") failed: ${e.message}", e) Log.e(TAG, "❌ refresh(\"$status\") failed: ${e.message}", e)
} }

View File

@ -28,6 +28,7 @@ import com.alya.ecommerce_serang.ui.order.detail.PaymentActivity
import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet import com.alya.ecommerce_serang.ui.order.history.cancelorder.CancelOrderBottomSheet
import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity import com.alya.ecommerce_serang.ui.order.review.CreateReviewActivity
import com.alya.ecommerce_serang.ui.product.ReviewProductActivity import com.alya.ecommerce_serang.ui.product.ReviewProductActivity
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
@ -41,7 +42,8 @@ import java.util.TimeZone
class OrderHistoryAdapter( class OrderHistoryAdapter(
private val onOrderClickListener: (OrdersItem) -> Unit, private val onOrderClickListener: (OrdersItem) -> Unit,
private val viewModel: HistoryViewModel, private val viewModel: HistoryViewModel,
private val callbacks: OrderActionCallbacks private val callbacks: OrderActionCallbacks,
private val listener: OnDialogActionListener
) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() { ) : RecyclerView.Adapter<OrderHistoryAdapter.OrderViewHolder>() {
interface OrderActionCallbacks { interface OrderActionCallbacks {
@ -237,9 +239,18 @@ class OrderHistoryAdapter(
callbacks.onShowLoading(true) callbacks.onShowLoading(true)
// Call ViewModel // Call ViewModel
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") viewModel.confirmOrderCompleted(order.orderId, "completed")
listener.onDialogConfirmed()
}
)
// viewModel.refreshOrders() // viewModel.refreshOrders()
} }
} }
@ -520,9 +531,19 @@ class OrderHistoryAdapter(
val bottomSheet = CancelOrderBottomSheet( val bottomSheet = CancelOrderBottomSheet(
orderId = orderId, orderId = orderId,
onOrderCancelled = { onOrderCancelled = {
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") callbacks.onOrderCancelled(orderId.toString(), true, "Order cancelled successfully")
// Show a success message // Show a success message
Toast.makeText(context, "Pesanan berhasil dibatalkan", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Pesanan berhasil dibatalkan", Toast.LENGTH_SHORT).show()
listener.onDialogConfirmed()
}
)
} }
) )
@ -603,3 +624,7 @@ class OrderHistoryAdapter(
} }
} }
} }
interface OnDialogActionListener {
fun onDialogConfirmed()
}

View File

@ -24,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.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks { class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks, OnDialogActionListener {
private var _binding: FragmentOrderListBinding? = null private var _binding: FragmentOrderListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -93,7 +93,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
navigateToOrderDetail(order) navigateToOrderDetail(order)
}, },
viewModel = viewModel, viewModel = viewModel,
callbacks = this // Pass this fragment as callback callbacks = this,
listener = this// Pass this fragment as callback
) )
orderAdapter.setFragmentStatus(status) orderAdapter.setFragmentStatus(status)
@ -175,7 +176,6 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) { override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
if (success) { if (success) {
Toast.makeText(requireContext(), "Berhasil batalkan pesanan", Toast.LENGTH_SHORT).show()
Log.d("OrderListFragment", "Order cancel success: $message") Log.d("OrderListFragment", "Order cancel success: $message")
// loadOrders() // Refresh the list // loadOrders() // Refresh the list
if (success) viewModel.updateStatus(status, forceRefresh = true) if (success) viewModel.updateStatus(status, forceRefresh = true)
@ -210,4 +210,14 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
super.onResume() super.onResume()
observeOrderList() observeOrderList()
} }
override fun onDialogConfirmed() {
viewModel.refresh(status)
// Option 1: refresh seluruh fragment
requireActivity().supportFragmentManager.beginTransaction()
.detach(this)
.attach(this)
.commit()
}
} }

View File

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

View File

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

View File

@ -1,18 +1,26 @@
package com.alya.ecommerce_serang.ui.product package com.alya.ecommerce_serang.ui.product
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat import androidx.appcompat.widget.SwitchCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -36,6 +44,9 @@ import com.alya.ecommerce_serang.ui.product.storeDetail.StoreDetailActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -51,6 +62,8 @@ class DetailProductActivity : AppCompatActivity() {
private var isWholesaleSelected: Boolean = false private var isWholesaleSelected: Boolean = false
private var minOrder: Int = 0 private var minOrder: Int = 0
private var TAG = "DetailProductActivity"
private val viewModel: ProductUserViewModel by viewModels { private val viewModel: ProductUserViewModel by viewModels {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
@ -292,6 +305,16 @@ class DetailProductActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image) .placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage) .into(binding.ivProductImage)
binding.ivProductImage.setOnClickListener {
val img = product.image
if (!img.isNullOrEmpty()){
showDetailProduct(img)
}else {
Toast.makeText(this, "Gambar tidak tersedia", Toast.LENGTH_SHORT).show()
Log.e(TAG, "There is no photo product")
}
}
val ratingStr = product.rating val ratingStr = product.rating
val ratingValue = ratingStr?.toFloatOrNull() val ratingValue = ratingStr?.toFloatOrNull()
@ -533,6 +556,59 @@ class DetailProductActivity : AppCompatActivity() {
) )
} }
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() { override fun onResume() {
super.onResume() super.onResume()
loadData() loadData()

View File

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

View File

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

View File

@ -39,6 +39,12 @@ class ChangePasswordActivity : AppCompatActivity() {
sessionManager = SessionManager(this) 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 // Listen for the result of the password change
viewModel.changePasswordResult.observe(this, Observer { result -> viewModel.changePasswordResult.observe(this, Observer { result ->
when (result) { when (result) {

View File

@ -1,6 +1,5 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.app.AlertDialog
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -27,6 +26,7 @@ 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.StoreOnReviewActivity
import com.alya.ecommerce_serang.ui.profile.mystore.StoreSuspendedActivity import com.alya.ecommerce_serang.ui.profile.mystore.StoreSuspendedActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
@ -115,7 +115,9 @@ class ProfileFragment : Fragment() {
when (store.approvalStatus) { when (store.approvalStatus) {
"process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java)) "process" -> startActivity(Intent(requireContext(), StoreOnReviewActivity::class.java))
"rejected" -> startActivity( "rejected" -> startActivity(
Intent(requireContext(), RegisterStoreActivity::class.java).putExtra("REAPPLY", true)) Intent(requireContext(), RegisterStoreActivity::class.java)
.putExtra("REAPPLY", true)
)
else -> { else -> {
when(store.storeStatus){ when(store.storeStatus){
"suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java)) "suspended" -> startActivity(Intent(requireContext(), StoreSuspendedActivity::class.java))
@ -198,14 +200,16 @@ class ProfileFragment : Fragment() {
private fun logout(){ private fun logout(){
AlertDialog.Builder(requireContext()) PopUpDialog.showConfirmDialog(
.setTitle("Konfirmasi") context = requireContext(),
.setMessage("Apakah anda yakin ingin keluar?") title = "Konfirmasi",
.setPositiveButton("Ya") { _, _ -> message = "Apakah anda yakin ingin keluar?",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
actionLogout() actionLogout()
} }
.setNegativeButton("Tidak", null) )
.show()
} }
private fun actionLogout(){ private fun actionLogout(){
@ -220,6 +224,7 @@ class ProfileFragment : Fragment() {
delay(500) delay(500)
loadingDialog.dismiss() loadingDialog.dismiss()
sessionManager.clearAll() sessionManager.clearAll()
viewModel.deleteFCM()
val intent = Intent(requireContext(), LoginActivity::class.java) val intent = Intent(requireContext(), LoginActivity::class.java)
startActivity(intent) startActivity(intent)
requireActivity().finish() requireActivity().finish()

View File

@ -2,7 +2,6 @@ package com.alya.ecommerce_serang.ui.profile.editprofile
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -29,6 +28,7 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityEditProfileCustBinding import com.alya.ecommerce_serang.databinding.ActivityEditProfileCustBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -213,12 +213,17 @@ class EditProfileCustActivity : AppCompatActivity() {
} }
private fun confirmUpdate() { private fun confirmUpdate() {
AlertDialog.Builder(this)
.setTitle("Konfirmasi Perubahan") PopUpDialog.showConfirmDialog(
.setMessage("Apakah Anda yakin ingin menyimpan perubahan profil toko Anda?") context = this,
.setPositiveButton("Ya") { _, _ -> saveProfile() } title = "Apakah Anda yakin ingin menyimpan perubahan profil?",
.setNegativeButton("Batal", null) message = "Pastikan data yang dimasukkan sudah benar",
.show() positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
saveProfile()
}
)
} }
private fun hasChanged(): Boolean { private fun hasChanged(): Boolean {

View File

@ -51,7 +51,6 @@ class MyStoreActivity : AppCompatActivity() {
enableEdgeToEdge() enableEdgeToEdge()
binding.headerMyStore.headerTitle.text = "Toko Saya" binding.headerMyStore.headerTitle.text = "Toko Saya"
binding.headerMyStore.headerLeftIcon.setOnClickListener { binding.headerMyStore.headerLeftIcon.setOnClickListener {
@ -107,22 +106,23 @@ class MyStoreActivity : AppCompatActivity() {
} }
binding.tvHistory.setOnClickListener { binding.tvHistory.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java)) //startActivity(Intent(this, SellsActivity::class.java))
startSellsActivityWithStatus("all")
} }
binding.layoutPerluTagihan.setOnClickListener { binding.layoutPerluTagihan.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java)) //startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("pending") startSellsActivityWithStatus("unpaid")
} }
binding.layoutPembayaran.setOnClickListener { binding.layoutPembayaran.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java)) //startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("paid") startSellsActivityWithStatus("paid")
} }
binding.layoutPerluDikirim.setOnClickListener { binding.layoutPerluDikirim.setOnClickListener {
startActivity(Intent(this, SellsActivity::class.java)) //startActivity(Intent(this, SellsActivity::class.java))
//navigateToSellsFragment("processed") startSellsActivityWithStatus("processed")
} }
binding.layoutProductMenu.setOnClickListener { binding.layoutProductMenu.setOnClickListener {
@ -206,6 +206,12 @@ 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() { override fun onResume() {
super.onResume() super.onResume()
lifecycleScope.launch { lifecycleScope.launch {

View File

@ -21,15 +21,16 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.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.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.MyStoreRepository import com.alya.ecommerce_serang.data.repository.MyStoreRepository
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding import com.alya.ecommerce_serang.databinding.ActivityRegisterStoreBinding
import com.alya.ecommerce_serang.ui.order.address.BankAdapter import com.alya.ecommerce_serang.ui.order.address.BankAdapter
import com.alya.ecommerce_serang.ui.order.address.CityAdapter import com.alya.ecommerce_serang.ui.order.address.CityAdapter
@ -38,7 +39,10 @@ import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.FileUtils import com.alya.ecommerce_serang.utils.FileUtils
import com.alya.ecommerce_serang.utils.ImageUtils 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.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -46,7 +50,6 @@ import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import androidx.core.net.toUri
class RegisterStoreActivity : AppCompatActivity() { class RegisterStoreActivity : AppCompatActivity() {
@ -58,6 +61,21 @@ class RegisterStoreActivity : AppCompatActivity() {
private lateinit var subdistrictAdapter: SubdsitrictAdapter private lateinit var subdistrictAdapter: SubdsitrictAdapter
private lateinit var bankAdapter: BankAdapter 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 // Request codes for file picking
private val PICK_STORE_IMAGE_REQUEST = 1001 private val PICK_STORE_IMAGE_REQUEST = 1001
private val PICK_KTP_REQUEST = 1002 private val PICK_KTP_REQUEST = 1002
@ -69,11 +87,7 @@ class RegisterStoreActivity : AppCompatActivity() {
private val LOCATION_PERMISSION_REQUEST = 2001 private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels { private val viewModel: RegisterStoreViewModel by viewModels {
BaseViewModelFactory { RegisterStoreViewModelFactory(this, intent.extras)
val apiService = ApiConfig.Companion.getApiService(sessionManager)
val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository)
}
} }
private val myStoreViewModel: MyStoreViewModel by viewModels { private val myStoreViewModel: MyStoreViewModel by viewModels {
@ -89,6 +103,30 @@ class RegisterStoreActivity : AppCompatActivity() {
binding = ActivityRegisterStoreBinding.inflate(layoutInflater) binding = ActivityRegisterStoreBinding.inflate(layoutInflater)
setContentView(binding.root) 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) sessionManager = SessionManager(this)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -123,6 +161,10 @@ class RegisterStoreActivity : AppCompatActivity() {
setupSpinners() // Location spinners setupSpinners() // Location spinners
Log.d(TAG, "onCreate: Spinners setup completed") Log.d(TAG, "onCreate: Spinners setup completed")
binding.checkboxApprove.setOnCheckedChangeListener { _, _ ->
validateRequiredFields()
}
// Setup observers // Setup observers
setupStoreTypesObserver() // Store type observer setupStoreTypesObserver() // Store type observer
setupObservers() setupObservers()
@ -209,12 +251,27 @@ class RegisterStoreActivity : AppCompatActivity() {
// Prefill spinner for store types // Prefill spinner for store types
preselectStoreType(store.storeTypeId) preselectStoreType(store.storeTypeId)
// Prefill province, city, and subdistrict // Cache what we want to select later (after data arrives)
preselectProvinceCitySubdistrict( wantedProvinceId = store.provinceId
provinceId = store.provinceId, wantedCityId = store.cityId
cityId = store.cityId, wantedSubdistrictId = store.subdistrict
subdistrictId = 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() validateRequiredFields()
} }
@ -225,65 +282,92 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
} else { } else {
binding.btnRegister.setOnClickListener { binding.btnRegister.setOnClickListener {
if (viewModel.validateForm()) viewModel.registerStore(this) if (viewModel.validateForm()){
else Toast.makeText(this, "Harap lengkapi semua field yang wajib diisi", Toast.LENGTH_SHORT).show() 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()
}
private fun preselectStoreType(storeTypeId: Int) { private fun preselectStoreType(storeTypeId: Int) {
// The adapter is created in setupStoreTypeSpinner(...) // The adapter is created in setupStoreTypeSpinner(...)
val adapter = binding.spinnerStoreType.adapter val adapter = binding.spinnerStoreType.adapter ?: return
if (adapter != null) { for (i in 0 until adapter.count) {
val count = adapter.count
for (i in 0 until count) {
val item = adapter.getItem(i) as? StoreTypesItem val item = adapter.getItem(i) as? StoreTypesItem
if (item?.id == storeTypeId) { if (item?.id == storeTypeId) {
binding.spinnerStoreType.setSelection(i, false) binding.spinnerStoreType.setSelection(i, false)
viewModel.storeTypeId.value = storeTypeId
validateRequiredFields()
break break
} }
} }
} }
}
private fun preselectProvinceCitySubdistrict( private fun tryApplyProvince() {
provinceId: Int, if (provinceApplied) return
cityId: String, val target = wantedProvinceId ?: return
subdistrictId: String val count = provinceAdapter.count
) { for (i in 0 until count) {
// Province first (this will trigger cities fetch) if (provinceAdapter.getProvinceId(i) == target) {
val provCount = provinceAdapter.count
for (i in 0 until provCount) {
if (provinceAdapter.getProvinceId(i) == provinceId) {
binding.spinnerProvince.setSelection(i, false) binding.spinnerProvince.setSelection(i, false)
break provinceApplied = true
maybeFinishRestoring()
return
}
} }
} }
// When cities arrive, select the city, then load subdistricts private fun tryApplyCity() {
viewModel.citiesState.observe(this) { state -> if (cityApplied) return
if (state is Result.Success) { val target = wantedCityId ?: return
val cityCount = cityAdapter.count val count = cityAdapter.count
for (i in 0 until cityCount) { for (i in 0 until count) {
if (cityAdapter.getCityId(i) == cityId) { if (cityAdapter.getCityId(i) == target) {
binding.spinnerCity.setSelection(i, false) binding.spinnerCity.setSelection(i, false)
break cityApplied = true
} maybeFinishRestoring()
return
} }
} }
} }
// When subdistricts arrive, select the subdistrict private fun tryApplySubdistrict() {
viewModel.subdistrictState.observe(this) { state -> if (subdistrictApplied) return
if (state is Result.Success) { val target = wantedSubdistrictId ?: return
val subCount = subdistrictAdapter.count val count = subdistrictAdapter.count
for (i in 0 until subCount) { for (i in 0 until count) {
if (subdistrictAdapter.getSubdistrictId(i) == subdistrictId) { if (subdistrictAdapter.getSubdistrictId(i) == target) {
binding.spinnerSubdistrict.setSelection(i, false) binding.spinnerSubdistrict.setSelection(i, false)
break 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
} }
} }
@ -301,31 +385,42 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
private fun validateRequiredFields() { private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() && val bankName = viewModel.bankName.value?.trim().orEmpty()
val bankSelected = bankName.isNotEmpty() && !bankName.equals("Pilih Bank", ignoreCase = true)
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.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 && (viewModel.postalCode.value ?: 0) > 0 &&
!viewModel.subdistrict.value.isNullOrBlank() && provinceSelected && citySelected && subdistrictSelected &&
!viewModel.bankName.value.isNullOrBlank() && storeTypeSelected &&
bankSelected &&
(viewModel.bankNumber.value ?: 0) > 0 && (viewModel.bankNumber.value ?: 0) > 0 &&
(viewModel.provinceId.value ?: 0) > 0 &&
!viewModel.cityId.value.isNullOrBlank() &&
(viewModel.storeTypeId.value ?: 0) > 0 &&
viewModel.ktpUri != null && viewModel.ktpUri != null &&
viewModel.nibUri != null && viewModel.nibUri != null &&
viewModel.npwpUri != null && viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() && viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank() !viewModel.accountName.value.isNullOrBlank() &&
binding.checkboxApprove.isChecked
binding.btnRegister.isEnabled = true binding.btnRegister.isEnabled = isFormValid
if (isFormValid) { binding.btnRegister.setBackgroundResource(
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active) if (isFormValid) R.drawable.bg_button_active else R.drawable.bg_button_disabled
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white)) )
binding.btnRegister.isEnabled = true binding.btnRegister.setTextColor(
} else { ContextCompat.getColor(this, if (isFormValid) R.color.white else R.color.black_300)
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled) )
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
binding.btnRegister.isEnabled = false
}
} }
private fun setupObservers() { private fun setupObservers() {
@ -340,12 +435,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerProvince.isEnabled = false binding.spinnerProvince.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar.visibility = View.GONE binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true binding.spinnerProvince.isEnabled = true
// Update adapter with data
provinceAdapter.updateData(state.data) provinceAdapter.updateData(state.data)
tryApplyProvince()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
@ -364,12 +457,10 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerCity.isEnabled = false binding.spinnerCity.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar.visibility = View.GONE binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true binding.spinnerCity.isEnabled = true
// Update adapter with data
cityAdapter.updateData(state.data) cityAdapter.updateData(state.data)
tryApplyCity()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
@ -387,11 +478,20 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerSubdistrict.isEnabled = false binding.spinnerSubdistrict.isEnabled = false
} }
is Result.Success -> { is Result.Success -> {
Log.d(TAG, "setupobservers: Subdistrict loaded successfullti: ${state.data.size} subdistrict")
binding.subdistrictProgressBar.visibility = View.GONE binding.subdistrictProgressBar.visibility = View.GONE
binding.spinnerSubdistrict.isEnabled = true binding.spinnerSubdistrict.isEnabled = true
subdistrictAdapter.updateData(state.data) subdistrictAdapter.updateData(state.data)
// If youre not restoring a specific subdistrict, select the first real item
if (!isRestoringSelections && state.data.isNotEmpty()) {
binding.spinnerSubdistrict.setSelection(0, false)
val id0 = subdistrictAdapter.getSubdistrictId(0)
viewModel.subdistrict.value = id0 ?: ""
viewModel.selectedSubdistrict = id0
validateRequiredFields()
}
tryApplySubdistrict()
} }
is Result.Error -> { is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}") Log.e(TAG, "setupObservers: Error loading subdistrict: ${state.exception.message}")
@ -466,6 +566,7 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup spinner with API data // Setup spinner with API data
setupStoreTypeSpinner(displayList) setupStoreTypeSpinner(displayList)
tryApplyBank()
} else { } else {
Log.w(TAG, "setupStoreTypesObserver: Received empty store types list") Log.w(TAG, "setupStoreTypesObserver: Received empty store types list")
} }
@ -510,17 +611,10 @@ class RegisterStoreActivity : AppCompatActivity() {
// Set item selection listener // Set item selection listener
binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerStoreType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
val selectedItem = adapter.getItem(position) val item = (binding.spinnerStoreType.adapter.getItem(pos) as? StoreTypesItem)
Log.d(TAG, "Store type selected: position=$position, item=${selectedItem?.name}, id=${selectedItem?.id}") if (item != null && item.id > 0) viewModel.storeTypeId.value = item.id
validateRequiredFields()
if (selectedItem != null && selectedItem.id > 0) {
// Store the actual ID from the API, not just position
viewModel.storeTypeId.value = selectedItem.id
Log.d(TAG, "Set storeTypeId to ${selectedItem.id}")
} else {
Log.d(TAG, "Default or null store type selected, not setting storeTypeId")
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -539,22 +633,12 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup province spinner // Setup province spinner
binding.spinnerProvince.adapter = provinceAdapter binding.spinnerProvince.adapter = provinceAdapter
binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerProvince.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
Log.d(TAG, "Province selected at position: $position") provinceAdapter.getProvinceId(pos)?.let {
val provinceId = provinceAdapter.getProvinceId(position) viewModel.provinceId.value = it
if (provinceId != null) { viewModel.getCities(it)
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")
} }
validateRequiredFields()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -565,21 +649,13 @@ class RegisterStoreActivity : AppCompatActivity() {
// Setup city spinner // Setup city spinner
binding.spinnerCity.adapter = cityAdapter binding.spinnerCity.adapter = cityAdapter
binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerCity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
Log.d(TAG, "City selected at position: $position") cityAdapter.getCityId(pos)?.let {
val cityId = cityAdapter.getCityId(position) viewModel.cityId.value = it
if (cityId != null) { viewModel.getSubdistrict(it)
Log.d(TAG, "Setting city ID: $cityId") viewModel.selectedCityId = it
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")
} }
validateRequiredFields()
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -590,16 +666,11 @@ class RegisterStoreActivity : AppCompatActivity() {
//Setup Subdistrict spinner //Setup Subdistrict spinner
binding.spinnerSubdistrict.adapter = subdistrictAdapter binding.spinnerSubdistrict.adapter = subdistrictAdapter
binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerSubdistrict.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
Log.d(TAG, "Subdistrict selected at position: $position") val selectedId = subdistrictAdapter.getSubdistrictId(pos)
val subdistrictId = subdistrictAdapter.getSubdistrictId(position) viewModel.subdistrict.value = selectedId ?: "" // empty => not selected
if (subdistrictId != null) { viewModel.selectedSubdistrict = selectedId
Log.d(TAG, "Setting subdistrict ID: $subdistrictId") validateRequiredFields()
viewModel.subdistrict.value = subdistrictId
viewModel.selectedSubdistrict = subdistrictId
} else {
Log.e(TAG, "Invalid subdistrict ID for position: $position")
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -609,63 +680,31 @@ class RegisterStoreActivity : AppCompatActivity() {
binding.spinnerBankName.adapter = bankAdapter binding.spinnerBankName.adapter = bankAdapter
binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerBankName.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected( override fun onItemSelected(p: AdapterView<*>?, v: View?, pos: Int, id: Long) {
parent: AdapterView<*>?, val bankName = bankAdapter.getBankName(pos)
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.bankName.value = bankName
viewModel.selectedBankName = bankName viewModel.selectedBankName = bankName
validateRequiredFields()
}
override fun onNothingSelected(parent: AdapterView<*>?) { /* no-op */ }
}
tryApplyBank()
// Optional: Log the selected bank details // // Add initial hints to the spinners
val selectedBank = bankAdapter.getBankItem(position) // if (provinceAdapter.isEmpty) provinceAdapter.add("Pilih Provinsi")
selectedBank?.let { // if (cityAdapter.isEmpty) cityAdapter.add("Pilih Kabupaten/Kota")
Log.d(TAG, "Selected bank: ${it.bankName} (Code: ${it.bankCode})") // if (subdistrictAdapter.isEmpty) subdistrictAdapter.add("Pilih Kecamatan")
} // if (bankAdapter.isEmpty) bankAdapter.add("Pilih Bank")
// Hide progress bar if it was showing
binding.bankNameProgressBar.visibility = View.GONE
} else {
Log.e(TAG, "Invalid bank name for position: $position")
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d(TAG, "No bank selected")
viewModel.selectedBankName = null
}
}
// Add initial hints to the spinners
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")
}
Log.d(TAG, "setupSpinners: Province and city spinners setup completed") Log.d(TAG, "setupSpinners: Province and city spinners setup completed")
} }
private fun maybeFinishRestoring() {
if (provinceApplied && cityApplied && subdistrictApplied) {
isRestoringSelections = false
}
}
private fun setupDocumentUploads() { private fun setupDocumentUploads() {
Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons") Log.d(TAG, "setupDocumentUploads: Setting up document upload buttons")
@ -793,18 +832,28 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
if (viewModel.storeName.value != s.toString()) {
viewModel.storeName.value = s.toString() viewModel.storeName.value = s.toString()
}
Log.d(TAG, "Store name updated: ${s.toString()}") Log.d(TAG, "Store name updated: ${s.toString()}")
validateRequiredFields() 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 { binding.etStoreDescription.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
viewModel.storeDescription.value = s.toString() viewModel.storeDescription.value = s.toString()
Log.d(TAG, "Store description updated: ${s.toString().take(20)}${if ((s?.length ?: 0) > 20) "..." else ""}") Log.d(TAG, "Store description updated: ${s.toString().take(20)}${if ((s?.length ?: 0) > 20) "..." else ""}")
validateRequiredFields()
} }
}) })
@ -812,53 +861,80 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
if (viewModel.street.value != s.toString()) {
viewModel.street.value = s.toString() viewModel.street.value = s.toString()
}
Log.d(TAG, "Street address updated: ${s.toString()}") Log.d(TAG, "Street address updated: ${s.toString()}")
validateRequiredFields() 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 { binding.etPostalCode.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
try { val newValue = s.toString().toIntOrNull() ?: 0
viewModel.postalCode.value = s.toString().toInt() if (viewModel.postalCode.value != newValue) {
Log.d(TAG, "Postal code updated: ${s.toString()}") viewModel.postalCode.value = newValue
} catch (e: NumberFormatException) { }
// Handle invalid input
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
validateRequiredFields() 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 { binding.etAddressDetail.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
if (viewModel.addressDetail.value != s.toString()) {
viewModel.addressDetail.value = s.toString() viewModel.addressDetail.value = s.toString()
}
Log.d(TAG, "Address detail updated: ${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 { binding.etBankNumber.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
val input = s.toString() val input = s.toString()
if (input.isNotEmpty()) { val newValue = if (input.isNotEmpty()) {
try { try {
viewModel.bankNumber.value = input.toInt() input.toInt()
Log.d(TAG, "Bank number updated: $input")
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
// Handle invalid input if needed
Log.e(TAG, "Failed to parse bank number. Input: $input, Error: $e") Log.e(TAG, "Failed to parse bank number. Input: $input, Error: $e")
0
} }
} else { } else {
// Handle empty input - perhaps set to 0 or null depending on your requirements 0
viewModel.bankNumber.value = 0 // or 0 }
Log.d(TAG, "Bank number set to default: 0")
if (viewModel.bankNumber.value != newValue) {
viewModel.bankNumber.value = newValue
Log.d(TAG, "Bank number updated: $newValue")
} }
validateRequiredFields() validateRequiredFields()
} }
@ -888,16 +964,27 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
if (viewModel.accountName.value != s.toString()) {
viewModel.accountName.value = s.toString() viewModel.accountName.value = s.toString()
}
Log.d(TAG, "Account Name updated: ${s.toString()}") Log.d(TAG, "Account Name updated: ${s.toString()}")
validateRequiredFields() 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") Log.d(TAG, "setupDataBinding: Text field data binding setup completed")
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode") Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode")
@ -993,30 +1080,41 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
private fun doUpdateStoreProfile() { private fun doUpdateStoreProfile() {
val nameBody: RequestBody = (viewModel.storeName.value ?: "") // --- Text parts ---
.toRequestBody("text/plain".toMediaTypeOrNull()) val nameBody: RequestBody = (viewModel.storeName.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()) val typeBody: RequestBody = ((viewModel.storeTypeId.value ?: 0).toString()).toRequestBody("text/plain".toMediaTypeOrNull())
.toRequestBody("text/plain".toMediaTypeOrNull()) val descBody: RequestBody = (viewModel.storeDescription.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
val descBody: RequestBody = (viewModel.storeDescription.value ?: "") // NOTE: is_on_leave is NOT part of approval multipart; keep separate if needed
.toRequestBody("text/plain".toMediaTypeOrNull())
val onLeaveBody: RequestBody = "false"
.toRequestBody("text/plain".toMediaTypeOrNull())
// --- Build Multipart for store image (optional) --- val latBody: RequestBody = (viewModel.latitude.value ?: "").toRequestBody("text/plain".toMediaTypeOrNull())
// Prefer compressing images to keep payload small; fall back to raw copy if needed. 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 -> val storeImgPart: MultipartBody.Part? = viewModel.storeImageUri?.let { uri ->
try { try {
// (A) Optional safety check: only allow jpg/png/webp
val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE) val allowed = Regex("^(jpg|jpeg|png|webp)$", RegexOption.IGNORE_CASE)
if (!ImageUtils.isAllowedFileType(this, uri, allowed)) { if (!ImageUtils.isAllowedFileType(this, uri, allowed)) {
Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Format gambar tidak didukung", Toast.LENGTH_SHORT).show()
null null
} else { } else {
// (B) Compress for upload (ke cacheDir), then build multipart
val compressed: File = ImageUtils.compressImage( val compressed: File = ImageUtils.compressImage(
context = this, context = this,
uri = uri, uri = uri,
filename = "storeimg", // prefix filename = "storeimg",
maxWidth = 1024, maxWidth = 1024,
maxHeight = 1024, maxHeight = 1024,
quality = 80 quality = 80
@ -1024,18 +1122,76 @@ class RegisterStoreActivity : AppCompatActivity() {
FileUtils.createMultipartFromFile("storeimg", compressed) FileUtils.createMultipartFromFile("storeimg", compressed)
} }
} catch (e: Exception) { } catch (e: Exception) {
// If compression fails, try raw copy as fallback
val rawFile = FileUtils.createTempFileFromUri(this, uri) val rawFile = FileUtils.createTempFileFromUri(this, uri)
rawFile?.let { FileUtils.createMultipartFromFile("storeimg", it) } rawFile?.let { FileUtils.createMultipartFromFile("storeimg", it) }
} }
} }
myStoreViewModel.updateStoreProfile( 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, storeName = nameBody,
storeType = typeBody,
description = descBody, description = descBody,
isOnLeave = onLeaveBody, storeType = typeBody,
storeImage = storeImgPart 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) { myStoreViewModel.updateStoreProfileResult.observe(this) {
@ -1049,6 +1205,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} }
} }
companion object { companion object {
private const val TAG = "RegisterStoreActivity" private const val TAG = "RegisterStoreActivity"
} }

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding import com.alya.ecommerce_serang.databinding.ActivityDetailStoreAddressBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.applyLiveCounter
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -55,6 +56,18 @@ class DetailStoreAddressActivity : AppCompatActivity() {
binding = ActivityDetailStoreAddressBinding.inflate(layoutInflater) binding = ActivityDetailStoreAddressBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
applyLiveCounter(
binding.edtStreet,
binding.tvCountStreet,
binding.tvCountStreetMax
)
applyLiveCounter(
binding.edtDetailAddress,
binding.tvCountDetail,
binding.tvCountDetailMax
)
sessionManager = SessionManager(this) sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager) apiService = ApiConfig.getApiService(sessionManager)

View File

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

View File

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

View File

@ -186,7 +186,9 @@ class SellsAdapter(
.into(ivSellsProduct) .into(ivSellsProduct)
tvSellsQty.text = "${order.orderItems?.size} produk" tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsPrice.text = order.totalAmount?.let { formatPrice(it.toDouble().toInt()) } val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
Log.d("SellsAdapter", "Cek price:$totalPrice" )
} }
"paid" -> { "paid" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -206,7 +208,9 @@ class SellsAdapter(
tvSellsTitle.text = "Pesanan Telah Dibayar" tvSellsTitle.text = "Pesanan Telah Dibayar"
tvSellsDueDesc.text = "Konfirmasi pembayaran sebelum:" tvSellsDueDesc.text = "Konfirmasi pembayaran sebelum:"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 2) 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" -> { "processed" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -235,6 +239,10 @@ class SellsAdapter(
tvSellsLocation.text = order.subdistrict tvSellsLocation.text = order.subdistrict
tvSellsCustomer.text = order.username tvSellsCustomer.text = order.username
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 2) 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" -> { "shipped" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -246,7 +254,11 @@ class SellsAdapter(
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0) tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive) tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
tvSellsQty.text = "${order.orderItems?.size} produk"
btnConfirmPayment.visibility = View.GONE btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
} }
"delivered" -> { "delivered" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -257,8 +269,13 @@ class SellsAdapter(
tvSellsDueDesc.text = "Dikirimkan pada" tvSellsDueDesc.text = "Dikirimkan pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0) tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive) tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
} }
"completed" -> { "completed" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -269,8 +286,13 @@ class SellsAdapter(
tvSellsDueDesc.text = "Selesai pada" tvSellsDueDesc.text = "Selesai pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0) tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0)
tvSellsQty.text = "${order.orderItems?.size} produk"
tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive) tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE btnConfirmPayment.visibility = View.GONE
val totalPrice = (order.totalAmount?.toDouble()?.toInt() ?: 0) - (order.shipmentPrice?.toDouble()?.toInt() ?: 0)
tvSellsPrice.text =formatPrice(totalPrice).toString()
} }
"canceled" -> { "canceled" -> {
layoutOrders.visibility = View.GONE layoutOrders.visibility = View.GONE
@ -281,6 +303,10 @@ class SellsAdapter(
tvSellsDueDesc.text = "Dibatalkan pada" tvSellsDueDesc.text = "Dibatalkan pada"
tvSellsDue.text = formatDueDate(order.updatedAt.toString(), 0) 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) tvSellsDue.background = itemView.context.getDrawable(R.drawable.bg_product_inactive)
btnConfirmPayment.visibility = View.GONE btnConfirmPayment.visibility = View.GONE
} }

View File

@ -17,8 +17,19 @@ import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
class SellsFragment : Fragment() { 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 _binding: FragmentSellsBinding? = null
// private var currentSearchQuery = ""
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var sessionManager: SessionManager private lateinit var sessionManager: SessionManager
@ -45,6 +56,8 @@ class SellsFragment : Fragment() {
sessionManager = SessionManager(requireContext()) sessionManager = SessionManager(requireContext())
setupViewPager() setupViewPager()
jumpToInitialStatusIfAny()
// binding.viewPagerSells.post { currentPage()?.filter(currentSearchQuery) }
} }
private fun setupViewPager() { private fun setupViewPager() {
@ -70,12 +83,60 @@ class SellsFragment : Fragment() {
statusPage() statusPage()
} }
private fun jumpToInitialStatusIfAny() {
val initial = arguments?.getString(ARG_INITIAL_STATUS)?.trim().orEmpty()
if (initial.isEmpty()) return
// Try adapters list first
var index = viewPagerAdapter.sellsStatuses.indexOf(initial)
// Fallback mapping (keeps working if the adapter changes order names)
if (index < 0) {
index = when (initial) {
"all" -> 0
"unpaid" -> 1
"paid" -> 2
"processed" -> 3
"shipped" -> 4
"delivered" -> 5
"completed" -> 6
"canceled" -> 7
else -> 0
}
}
if (index in 0 until (binding.viewPagerSells.adapter?.itemCount ?: 0)) {
// Ensure pager is ready, then jump without animation
binding.viewPagerSells.post {
binding.viewPagerSells.setCurrentItem(index, false)
// Make sure ViewModel filter matches the shown tab
sellsVm.updateStatus(
viewPagerAdapter.sellsStatuses.getOrNull(index) ?: initial,
forceRefresh = true
)
}
}
}
// fun onSearchQueryChanged(q: String) {
// currentSearchQuery = q
// currentPage()?.filter(q)
// }
// private fun currentPage(): SellsListFragment? {
// val pos = binding.viewPagerSells.currentItem
// val tag = "f${viewPagerAdapter.getItemId(pos)}" // requires stable ids in adapter (see step 4)
// return childFragmentManager.findFragmentByTag(tag) as? SellsListFragment
// }
private fun statusPage() { private fun statusPage() {
binding.viewPagerSells.registerOnPageChangeCallback( binding.viewPagerSells.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
val status = viewPagerAdapter.sellsStatuses[position] val status = viewPagerAdapter.sellsStatuses[position]
sellsVm.updateStatus(status, forceRefresh = true) sellsVm.updateStatus(status, forceRefresh = true)
// re-apply query when user switches tab
// currentPage()?.filter(currentSearchQuery)
} }
} }
) )

View File

@ -25,6 +25,7 @@ import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale
class SellsListFragment : Fragment() { class SellsListFragment : Fragment() {
@ -41,6 +42,13 @@ class SellsListFragment : Fragment() {
} }
private lateinit var sellsAdapter: SellsAdapter private lateinit var sellsAdapter: SellsAdapter
private var status: String = "all" private var status: String = "all"
// private var allOrders: List<OrdersItem> = emptyList()
// private var currentQuery: String = ""
//
// fun filter(query: String) {
// currentQuery = query
// applyFilter()
// }
companion object { companion object {
private const val TAG = "SellsListFragment" private const val TAG = "SellsListFragment"
@ -89,6 +97,29 @@ class SellsListFragment : Fragment() {
observePaymentConfirmation() observePaymentConfirmation()
} }
// 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() { private fun setupRecyclerView() {
Log.d(TAG, "Setting up RecyclerView") Log.d(TAG, "Setting up RecyclerView")
sellsAdapter = SellsAdapter( sellsAdapter = SellsAdapter(
@ -118,6 +149,8 @@ class SellsListFragment : Fragment() {
is ViewState.Success -> { is ViewState.Success -> {
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
Log.d(TAG, "Data received: ${result.data?.size ?: 0} items") 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()) { if (result.data.isNullOrEmpty()) {
binding.rvSells.visibility = View.GONE binding.rvSells.visibility = View.GONE
@ -135,18 +168,17 @@ class SellsListFragment : Fragment() {
sellsAdapter.submitList(result.data) sellsAdapter.submitList(result.data)
Log.d(TAG, "Data submitted to adapter") 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 -> { is ViewState.Error -> {
Log.e(TAG, "❌ ViewState.Error received: ${result.message}") Log.e(TAG, "❌ ViewState.Error received: ${result.message}")
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
binding.tvEmptyState.visibility = View.VISIBLE binding.tvEmptyState.visibility = View.VISIBLE
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show() // Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
}
is ViewState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
} }
is ViewState.Loading -> binding.progressBar.visibility = View.VISIBLE
} }
} }
} }
@ -213,7 +245,7 @@ class SellsListFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.getSellList(status) viewModel.getSellList(status)
observeSellsList() //observeSellsList()
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -26,4 +26,7 @@ class SellsViewPagerAdapter(
// Create a new instance of SellsListFragment with the appropriate status // Create a new instance of SellsListFragment with the appropriate status
return SellsListFragment.newInstance(sellsStatuses[position]) return SellsListFragment.newInstance(sellsStatuses[position])
} }
// override fun getItemId(position: Int): Long = sellsStatuses[position].hashCode().toLong()
// override fun containsItem(itemId: Long): Boolean =
// sellsStatuses.any { it.hashCode().toLong() == itemId }
} }

View File

@ -27,6 +27,7 @@ import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailPaymentBinding import com.alya.ecommerce_serang.databinding.ActivityDetailPaymentBinding
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsProductAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.PopUpDialog
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel import com.alya.ecommerce_serang.utils.viewmodel.AddressViewModel
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
@ -109,8 +110,18 @@ class DetailPaymentActivity : AppCompatActivity() {
binding.btnConfirmPayment.setOnClickListener { binding.btnConfirmPayment.setOnClickListener {
sells?.orderId?.let { sells?.orderId?.let {
PopUpDialog.showConfirmDialog(
context = this,
title = "Apakah anda yakin?",
message = "Pastikan data pembayaran sudah sesuai",
positiveText = "Ya",
negativeText = "Tidak",
onYesClicked = {
viewModel.confirmPayment(it, "confirmed") viewModel.confirmPayment(it, "confirmed")
Toast.makeText(this, "Pembayaran dikonfirmasi", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Pembayaran dikonfirmasi", Toast.LENGTH_SHORT).show()
}
)
} ?: run { } ?: run {
Log.e("DetailPaymentActivity", "No order passed in intent") Log.e("DetailPaymentActivity", "No order passed in intent")
} }
@ -134,7 +145,8 @@ class DetailPaymentActivity : AppCompatActivity() {
tvOrderCustomer.text = sell.username tvOrderCustomer.text = sell.username
tvOrderDate.text = formatDate(sell.updatedAt.toString()) tvOrderDate.text = formatDate(sell.updatedAt.toString())
tvOrderTotalProduct.text = "(${sell.orderItems?.size ?: 0} Barang)" 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()) tvOrderShipPrice.text = formatPrice(sell.shipmentPrice.toString())
tvOrderPrice.text = formatPrice(sell.totalAmount.toString()) tvOrderPrice.text = formatPrice(sell.totalAmount.toString())
tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2) tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2)

View File

@ -101,7 +101,8 @@ class DetailShipmentActivity : AppCompatActivity() {
tvOrderCustomer.text = sell.username tvOrderCustomer.text = sell.username
tvOrderDate.text = formatDate(sell.updatedAt.toString()) tvOrderDate.text = formatDate(sell.updatedAt.toString())
tvOrderTotalProduct.text = "(${sell.orderItems?.size ?: 0} Barang)" 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()) tvOrderShipPrice.text = formatPrice(sell.shipmentPrice.toString())
tvOrderPrice.text = formatPrice(sell.totalAmount.toString()) tvOrderPrice.text = formatPrice(sell.totalAmount.toString())
tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2) tvOrderDue.text = formatDueDate(sell.updatedAt.toString(), 2)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.PaymentUpdate
import com.alya.ecommerce_serang.data.api.dto.ProductsItem import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
@ -19,6 +20,7 @@ import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.NumberFormat import java.text.NumberFormat
import java.util.Locale import java.util.Locale
@ -199,6 +201,80 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
} }
} }
private fun String.toRequestBody(): RequestBody = private fun String.toPlain(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this) this.toRequestBody("text/plain".toMediaTypeOrNull())
fun updateStoreApproval(
storeName: RequestBody,
description: RequestBody,
storeType: RequestBody,
latitude: RequestBody,
longitude: RequestBody,
storeProvince: RequestBody,
storeCity: RequestBody,
storeSubdistrict: RequestBody,
storeVillage: RequestBody,
storeStreet: RequestBody,
storePostalCode: RequestBody,
storeAddressDetail: RequestBody,
userPhone: RequestBody,
paymentsToUpdate: List<PaymentUpdate> = emptyList(),
paymentIdToDelete: List<Int> = emptyList(),
storeCourier: List<String>? = null,
storeImage: MultipartBody.Part?,
ktpImage: MultipartBody.Part?,
npwpDocument: MultipartBody.Part?,
nibDocument: MultipartBody.Part?
) {
viewModelScope.launch {
try {
val store = myStoreProfile.value
if (store == null) {
_errorMessage.postValue("Data toko tidak tersedia")
Log.e(TAG, "Store data is null")
return@launch
}
val response = repository.updateStoreApproval(
storeName = storeName,
description = description,
storeType = storeType,
latitude = latitude,
longitude = longitude,
storeProvince = storeProvince,
storeCity = storeCity,
storeSubdistrict = storeSubdistrict,
storeVillage = storeVillage,
storeStreet = storeStreet,
storePostalCode = storePostalCode,
storeAddressDetail = storeAddressDetail,
userPhone = userPhone,
paymentsToUpdate = paymentsToUpdate,
paymentIdToDelete = paymentIdToDelete,
storeCourier = storeCourier,
storeImage = storeImage,
ktpImage = ktpImage,
npwpDocument = npwpDocument,
nibDocument = nibDocument
)
if (response != null) {
if (response.isSuccessful) {
_updateStoreProfileResult.postValue(response.body())
Log.d(TAG, "Update successful: ${response.body()}")
} else {
_errorMessage.postValue("Gagal memperbarui profil")
Log.e(TAG, "Update failed: ${response.errorBody()?.string()}")
}
} else {
_errorMessage.postValue("Terjadi kesalahan jaringan atau server")
Log.e(TAG, "Repository returned null response")
}
} catch (e: Exception) {
_errorMessage.postValue(e.message ?: "Unexpected error")
Log.e(TAG, "Exception updating store profile", e)
}
}
}
} }

View File

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.auth.ChangePassResponse 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.auth.HasStoreResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.repository.Result import com.alya.ecommerce_serang.data.repository.Result
@ -27,6 +28,10 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
private val _checkStore = MutableLiveData<Boolean>() private val _checkStore = MutableLiveData<Boolean>()
val checkStore: LiveData<Boolean> = _checkStore val checkStore: LiveData<Boolean> = _checkStore
private val _deleteFCMT = MutableLiveData<String>()
val deleteFCMT: LiveData<String> = _deleteFCMT
val changePasswordResult = MutableLiveData<Result<ChangePassResponse>>() val changePasswordResult = MutableLiveData<Result<ChangePassResponse>>()
private val _logout = MutableLiveData<Boolean>() private val _logout = MutableLiveData<Boolean>()
val logout : LiveData<Boolean> = _logout val logout : LiveData<Boolean> = _logout
@ -61,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( fun editProfileDirect(
context: Context, context: Context,

View File

@ -5,6 +5,7 @@ import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse 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 import kotlinx.coroutines.launch
class RegisterStoreViewModel( class RegisterStoreViewModel(
private val repository: UserRepository private val repository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() { ) : ViewModel() {
// LiveData for UI state // LiveData for UI state
@ -56,20 +58,20 @@ class RegisterStoreViewModel(
var selectedBankName: String? = null var selectedBankName: String? = null
// Form fields // Form fields
val storeName = MutableLiveData<String>() val storeName: MutableLiveData<String> = savedStateHandle.getLiveData("storeName", "")
val storeDescription = MutableLiveData<String>() val storeDescription: MutableLiveData<String> = savedStateHandle.getLiveData("storeDescription", "")
val storeTypeId = MutableLiveData<Int>() val storeTypeId: MutableLiveData<Int> = savedStateHandle.getLiveData("storeTypeId", 0)
val latitude = MutableLiveData<String>() val latitude: MutableLiveData<String> = savedStateHandle.getLiveData("latitude", "")
val longitude = MutableLiveData<String>() val longitude: MutableLiveData<String> = savedStateHandle.getLiveData("longitude", "")
val street = MutableLiveData<String>() val street: MutableLiveData<String> = savedStateHandle.getLiveData("street", "")
val subdistrict = MutableLiveData<String>() val subdistrict: MutableLiveData<String> = savedStateHandle.getLiveData("subdistrict", "")
val cityId = MutableLiveData<String>() val cityId: MutableLiveData<String> = savedStateHandle.getLiveData("cityId", "")
val provinceId = MutableLiveData<Int>() val provinceId: MutableLiveData<Int> = savedStateHandle.getLiveData("provinceId", 0)
val postalCode = MutableLiveData<Int>() val postalCode: MutableLiveData<Int> = savedStateHandle.getLiveData("postalCode", 0)
val addressDetail = MutableLiveData<String>() val addressDetail: MutableLiveData<String> = savedStateHandle.getLiveData("addressDetail", "")
val bankName = MutableLiveData<String>() val bankName: MutableLiveData<String> = savedStateHandle.getLiveData("bankName", "")
val bankNumber = MutableLiveData<Int>() val bankNumber: MutableLiveData<Int> = savedStateHandle.getLiveData("bankNumber", 0)
val accountName = MutableLiveData<String>() val accountName: MutableLiveData<String> = savedStateHandle.getLiveData("accountName", "")
// Files // Files
var storeImageUri: Uri? = null var storeImageUri: Uri? = null
@ -79,6 +81,15 @@ class RegisterStoreViewModel(
var persetujuanUri: Uri? = null var persetujuanUri: Uri? = null
var qrisUri: 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 // Selected couriers
val selectedCouriers = mutableListOf<String>() val selectedCouriers = mutableListOf<String>()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

View File

@ -0,0 +1,11 @@
<?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="120dp"
android:height="120dp"
android:drawable="@drawable/logo_psb_crop"
android:gravity="center" />
</layer-list>

View File

@ -85,14 +85,45 @@
android:id="@+id/etDetailAlamat" android:id="@+id/etDetailAlamat"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:background="@drawable/bg_text_field"
android:background="@drawable/edit_text_background" android:padding="8dp"
android:gravity="top" style="@style/body_small"
android:hint="Isi detail alamat (nomor rumah, lantai, dll)" android:hint="Isi detail alamat di sini, contoh: Blok, No. Kavling, dsb."
android:inputType="textMultiLine" android:layout_marginTop="10dp"/>
android:lines="3"
android:padding="12dp" <LinearLayout
android:textSize="14sp" /> android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_detail_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="100"
android:textColor="@color/black_300"/>
</LinearLayout>
<!-- Provinsi --> <!-- Provinsi -->
<TextView <TextView
@ -200,7 +231,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="Desa" android:text="Desa / Kelurahan"
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textSize="14sp" /> android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@ -312,7 +343,7 @@
<Button <Button
android:id="@+id/btnReloadLocation" android:id="@+id/btnReloadLocation"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="36dp" android:layout_height="36dp"
android:text="Reload" android:text="Reload"
android:textSize="12sp" android:textSize="12sp"

View File

@ -28,10 +28,10 @@
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"
android:contentDescription="Category Header Image" /> android:contentDescription="Category Header Image" />
<View <!-- <View-->
android:layout_width="match_parent" <!-- android:layout_width="match_parent"-->
android:layout_height="match_parent" <!-- android:layout_height="match_parent"-->
android:background="@color/blue_50" /> <!-- android:background="@color/blue_50" />-->
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"

View File

@ -10,7 +10,7 @@
tools:context=".ui.profile.ChangePasswordActivity"> tools:context=".ui.profile.ChangePasswordActivity">
<include <include
android:id="@+id/headerStoreProduct" android:id="@+id/header"
layout="@layout/header" /> layout="@layout/header" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View File

@ -411,11 +411,25 @@
<Button <Button
android:id="@+id/btn_hold_payment" android:id="@+id/btn_hold_payment"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:layout_weight="1"
android:scrollHorizontally="false"
android:singleLine="false"
style="@style/button.large.secondary.medium" style="@style/button.large.secondary.medium"
android:text="Tahan Konfirmasi"/> android:text="Tahan Konfirmasi"/>
<Button <Button
android:id="@+id/btn_confirm_payment" android:id="@+id/btn_confirm_payment"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
android:layout_weight="1"
android:scrollHorizontally="false"
android:singleLine="false"
style="@style/button.large.active.medium" style="@style/button.large.active.medium"
android:text="Konfirmasi Terima" android:text="Konfirmasi Terima"
android:layout_alignParentEnd="true"/> android:layout_alignParentEnd="true"/>

View File

@ -31,8 +31,9 @@
<ImageView <ImageView
android:id="@+id/ivProductImage" android:id="@+id/ivProductImage"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="360dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:layout_marginTop="4dp"
android:contentDescription="@string/product_image" android:contentDescription="@string/product_image"
tools:src="@drawable/placeholder_image" /> tools:src="@drawable/placeholder_image" />
@ -218,7 +219,7 @@
android:layout_weight="1" android:layout_weight="1"
android:text="@string/ulasan_pembeli" android:text="@string/ulasan_pembeli"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="14sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
@ -276,7 +277,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/detail_produk" android:text="@string/detail_produk"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="14sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TableLayout <TableLayout
@ -357,7 +358,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/deskripsi_produk" android:text="@string/deskripsi_produk"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="14sp" android:textSize="16sp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:textStyle="bold" /> android:textStyle="bold" />
@ -421,7 +422,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="14sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
tools:text="SnackEnak" /> tools:text="SnackEnak" />
@ -484,7 +485,7 @@
android:layout_weight="1" android:layout_weight="1"
android:text="@string/produk_lainnya" android:text="@string/produk_lainnya"
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="14sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView

View File

@ -205,7 +205,6 @@
</LinearLayout> </LinearLayout>
<!-- Jalan --> <!-- Jalan -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -223,13 +222,49 @@
<EditText <EditText
android:id="@+id/edt_street" android:id="@+id/edt_street"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="70dp"
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi nama jalan di sini"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:hint="Isi nama jalan di sini" android:inputType="text|textMultiLine"
android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_street"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_street_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="255"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Kode Pos --> <!-- Kode Pos -->
@ -254,6 +289,7 @@
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:hint="Isi kode pos di sini" android:hint="Isi kode pos di sini"
android:inputType="number"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
</LinearLayout> </LinearLayout>
@ -275,15 +311,47 @@
<EditText <EditText
android:id="@+id/edt_detail_address" android:id="@+id/edt_detail_address"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="70dp" android:layout_height="wrap_content"
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi detail alamat di sini, contoh: Blok, No. Kavling, dsb."
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:inputType="text|textMultiLine" android:hint="Isi detail alamat di sini, contoh: Blok, No. Kavling, dsb."
android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_detail_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="100"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Pinpoint Lokasi --> <!-- Pinpoint Lokasi -->
@ -379,6 +447,10 @@
<Button <Button
android:id="@+id/btn_save_address" android:id="@+id/btn_save_address"
android:text="Simpan Perubahan" android:text="Simpan Perubahan"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_gravity="center"
android:gravity="center"
style="@style/button.large.active.long" style="@style/button.large.active.long"
android:enabled="true" android:enabled="true"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="16dp"/>

View File

@ -167,6 +167,40 @@
style="@style/body_small" style="@style/body_small"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_name_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="50"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Kategori Produk --> <!-- Kategori Produk -->
@ -271,6 +305,40 @@
android:gravity="top" android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_desc_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="255"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Harga Produk --> <!-- Harga Produk -->
@ -371,6 +439,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi stok produk di sini" android:hint="Isi stok produk di sini"
android:padding="8dp" android:padding="8dp"
android:inputType="number"
style="@style/body_small" /> style="@style/body_small" />
</LinearLayout> </LinearLayout>
@ -413,6 +482,7 @@
android:hint="Isi minimum pemesanan produk di sini" android:hint="Isi minimum pemesanan produk di sini"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:inputType="number"
android:layout_marginTop="10dp" /> android:layout_marginTop="10dp" />
</LinearLayout> </LinearLayout>
@ -711,6 +781,7 @@
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi minimum produk untuk mendapatkan harga grosir di sini" android:hint="Isi minimum produk untuk mendapatkan harga grosir di sini"
android:padding="8dp" android:padding="8dp"
android:inputType="number"
style="@style/body_small" /> style="@style/body_small" />
</LinearLayout> </LinearLayout>

View File

@ -138,6 +138,40 @@
style="@style/body_small" style="@style/body_small"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_name_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="30"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Deskripsi Toko --> <!-- Deskripsi Toko -->
@ -166,6 +200,40 @@
android:gravity="top" android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_desc_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="255"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Jenis UMKM --> <!-- Jenis UMKM -->
@ -435,7 +503,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginBottom="24dp"> android:layout_marginVertical="24dp">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -463,13 +531,49 @@
<EditText <EditText
android:id="@+id/et_street" android:id="@+id/et_street"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="70dp"
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi jalan tempat toko Anda di sini" android:hint="Isi jalan tempat toko Anda di sini"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:inputType="text|textMultiLine"
android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:layout_marginTop="4dp">
<TextView
android:id="@+id/tv_count_street"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="/"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView
android:id="@+id/tv_count_street_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/label_small"
android:text="255"
android:textColor="@color/black_300"/>
</LinearLayout>
</LinearLayout> </LinearLayout>
<!-- Kode Pos --> <!-- Kode Pos -->
@ -532,58 +636,49 @@
<EditText <EditText
android:id="@+id/et_address_detail" android:id="@+id/et_address_detail"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="70dp" android:layout_height="wrap_content"
android:background="@drawable/bg_text_field" android:background="@drawable/bg_text_field"
android:hint="Isi detail alamat toko Anda di sini" android:hint="Isi detail alamat toko Anda di sini"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" style="@style/body_small"
android:inputType="text|textMultiLine" android:inputType="text"
android:gravity="top" android:gravity="top"
android:layout_marginTop="10dp"/> android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Bank -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal"
android:layout_marginBottom="24dp" android:layout_gravity="end"
android:visibility="gone"> android:layout_marginTop="4dp">
<LinearLayout <TextView
android:layout_width="match_parent" android:id="@+id/tv_count_address_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> style="@style/label_small"
android:text="0"
android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="10. Bank" style="@style/label_small"
style="@style/body_medium" android:text="/"
android:layout_marginEnd="4dp"/> android:layout_marginEnd="2dp"
android:textColor="@color/black_300"/>
<TextView <TextView
android:layout_width="0dp" android:id="@+id/tv_count_address_detail_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" style="@style/label_small"
android:text="*" android:text="100"
style="@style/body_medium" android:textColor="@color/black_300"/>
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout> </LinearLayout>
<EditText
android:id="@+id/et_bank_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama bank untuk toko Anda di sini"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout> </LinearLayout>
<!-- Nama Bank--> <!-- Nama Bank-->
@ -654,6 +749,48 @@
</LinearLayout> </LinearLayout>
<!-- Nama Pemilik Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="11. Nama Pemilik Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama pemilik rekening"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Nomor Rekening --> <!-- Nomor Rekening -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -669,7 +806,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="11. Nomor Rekening" android:text="12. Nomor Rekening"
style="@style/body_medium" style="@style/body_medium"
android:layout_marginEnd="4dp"/> android:layout_marginEnd="4dp"/>
@ -697,48 +834,6 @@
</LinearLayout> </LinearLayout>
<!-- Nama Pemilik Rekening -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12. Nama Pemilik Rekening"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<EditText
android:id="@+id/et_account_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_text_field"
android:hint="Isi nama pemilik rekening"
android:padding="8dp"
style="@style/body_small"
android:layout_marginTop="10dp"/>
</LinearLayout>
<!-- Kurir --> <!-- Kurir -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -915,73 +1010,6 @@
</LinearLayout> </LinearLayout>
<!-- NIB -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="16. Dokumen NIB"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<FrameLayout
android:id="@+id/container_nib"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<!-- NPWP --> <!-- NPWP -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -997,7 +1025,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="17. Dokumen NPWP" android:text="16. Dokumen NPWP"
style="@style/body_medium" style="@style/body_medium"
android:layout_marginEnd="4dp"/> android:layout_marginEnd="4dp"/>
@ -1049,6 +1077,73 @@
</LinearLayout> </LinearLayout>
<!-- NIB -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="17. Dokumen NIB"
style="@style/body_medium"
android:layout_marginEnd="4dp"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="*"
style="@style/body_medium"
android:textColor="@color/red_required"
android:layout_gravity="end"/>
</LinearLayout>
<FrameLayout
android:id="@+id/container_nib"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="8dp"
android:background="@android:drawable/editbox_background">
<ImageView
android:id="@+id/img_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerInside"
android:visibility="gone" />
<LinearLayout
android:id="@+id/layout_upload_nib"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_upload" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Unggah dokumen Anda di sini"
android:textColor="#777777" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -1058,7 +1153,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="17. Pilih Titik Lokasi Usaha" android:text="18. Pilih Titik Lokasi Usaha"
style="@style/body_medium" style="@style/body_medium"
android:layout_marginEnd="4dp"/> android:layout_marginEnd="4dp"/>

View File

@ -17,7 +17,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:padding="16dp"
android:background="@color/white"> android:background="@color/white"
android:visibility="gone">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -22,6 +22,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:textSize="14sp"
android:text="Bukti Pembayaran" android:text="Bukti Pembayaran"
style="@style/title_large" /> style="@style/title_large" />

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
app:cardElevation="5dp"
app:cardCornerRadius="15dp"
app:cardBackgroundColor="@color/white"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<ImageView
android:id="@+id/dialogIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_gravity="center_horizontal"
android:contentDescription="icon dialog"
android:visibility="gone" />
<TextView
android:id="@+id/dialogTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Judul"
android:layout_marginTop="32dp"
android:paddingHorizontal="16dp"
android:layout_marginHorizontal="32dp"
android:paddingTop="4dp"
android:ellipsize="end"
android:scrollHorizontally="false"
android:singleLine="false"
android:gravity="center"
android:textSize="18sp"
android:maxLines="3"
android:fontFamily="@font/dmsans_semibold"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/dialogMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginHorizontal="32dp"
android:gravity="center"
android:maxLines="3"
android:paddingHorizontal="16dp"
android:paddingTop="4dp"
android:ellipsize="end"
android:scrollHorizontally="false"
android:singleLine="false"
android:text="Pesan Dialog"
android:fontFamily="@font/dmsans_regular"
android:textSize="14sp"
android:textColor="?attr/colorOnSurfaceVariant" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:layout_marginBottom="16dp"
android:paddingHorizontal="16dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnNo"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_regular"
android:theme="@style/body_medium"
android:text="Tidak" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnYes"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:fontFamily="@font/dmsans_regular"
android:theme="@style/body_medium"
android:text="Ya" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -37,6 +37,7 @@
android:background="@null" android:background="@null"
android:hint="Isi stok produk di sini" android:hint="Isi stok produk di sini"
android:inputType="number" android:inputType="number"
android:textAlignment="center"
android:padding="8dp" android:padding="8dp"
style="@style/body_small" /> style="@style/body_small" />

View File

@ -7,12 +7,28 @@
android:theme="@style/Theme.Ecommerce_serang" android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.home.HomeFragment"> tools:context=".ui.home.HomeFragment">
<ImageView
android:id="@+id/logo_psb_home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="4dp"
android:src="@drawable/logo_home"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintEnd_toStartOf="@id/searchContainer" />
<include <include
android:id="@+id/searchContainer" android:id="@+id/searchContainer"
layout="@layout/view_search" layout="@layout/view_search"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toEndOf="@id/logo_psb_home"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
@ -21,6 +37,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:fillViewport="true" android:fillViewport="true"
android:overScrollMode="never" android:overScrollMode="never"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/searchContainer" app:layout_constraintTop_toBottomOf="@id/searchContainer"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">

View File

@ -25,7 +25,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:text="Gracia Hotmauli" android:text="Nama Pengguna"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/profileImage" app:layout_constraintStart_toEndOf="@id/profileImage"
@ -35,7 +35,7 @@
android:id="@+id/tvUsername" android:id="@+id/tvUsername"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="\@gracia34" android:text="Username"
android:textColor="#757575" android:textColor="#757575"
app:layout_constraintStart_toStartOf="@id/tvName" app:layout_constraintStart_toStartOf="@id/tvName"
app:layout_constraintTop_toBottomOf="@id/tvName" /> app:layout_constraintTop_toBottomOf="@id/tvName" />

View File

@ -3,8 +3,6 @@
xmlns:card_view="http://schemas.android.com/apk/res-auto" xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="12dp"
card_view:cardElevation="6dp" card_view:cardElevation="6dp"
android:foreground="?android:attr/selectableItemBackground"> android:foreground="?android:attr/selectableItemBackground">
@ -18,17 +16,10 @@
android:id="@+id/tvOption" android:id="@+id/tvOption"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/dmsans_semibold" android:fontFamily="@font/dmsans_regular"
android:text="Item 1" android:text="Item 1"
android:textColor="@color/black_500"
android:textSize="16sp" /> android:textSize="16sp" />
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
android:layout_marginTop="4dp" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View File

@ -133,23 +133,36 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_left" android:id="@+id/btn_left"
android:layout_width="wrap_content" android:layout_width="120dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_button_outline" android:background="@drawable/bg_button_outline"
android:backgroundTint="@color/white" android:backgroundTint="@color/white"
android:maxLines="2"
android:ellipsize="end"
android:scrollHorizontally="false"
android:singleLine="false"
android:padding="4dp"
android:visibility="gone" android:visibility="gone"
android:text="Tidak"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate" app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp"/> android:layout_marginTop="8dp"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_right" android:id="@+id/btn_right"
android:layout_width="wrap_content" android:layout_width="120dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_button_filled" android:background="@drawable/bg_button_filled"
android:maxLines="2"
android:ellipsize="end"
android:visibility="gone" android:visibility="gone"
android:scrollHorizontally="false"
android:singleLine="false"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:text="Kirim Bukti Bayar" android:text="Ya"
app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate" app:layout_constraintTop_toBottomOf="@id/tvDeadlineDate"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp"/> android:layout_marginTop="8dp"/>

View File

@ -39,7 +39,7 @@
<TextView <TextView
android:id="@+id/courier_name_cost" android:id="@+id/courier_name_cost"
android:fontFamily="@font/dmsans_semibold" android:fontFamily="@font/dmsans_semibold"
android:textSize="14sp" android:textSize="16sp"
android:paddingHorizontal="2dp" android:paddingHorizontal="2dp"
android:paddingTop="4dp" android:paddingTop="4dp"
android:ellipsize="end" android:ellipsize="end"
@ -54,7 +54,7 @@
android:id="@+id/est_date" android:id="@+id/est_date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="12sp" android:textSize="14sp"
android:paddingHorizontal="4dp" android:paddingHorizontal="4dp"
android:text="Estimasi 3-4 hari"/> android:text="Estimasi 3-4 hari"/>
</LinearLayout> </LinearLayout>
@ -65,7 +65,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="0.3" android:layout_weight="0.3"
android:textSize="14sp" android:textSize="16sp"
android:gravity="start" android:gravity="start"
android:fontFamily="@font/dmsans_semibold" android:fontFamily="@font/dmsans_semibold"
android:text="Rp15.000"/> android:text="Rp15.000"/>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

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