Compare commits

...

37 Commits

Author SHA1 Message Date
ed60528049 add account name 2025-08-12 00:44:35 +07:00
a9c2f9c103 fix validation file upliad 2025-08-11 22:47:00 +07:00
eab3884fd6 Merge remote-tracking branch 'origin/master' 2025-08-11 20:32:11 +07:00
930688f50d update postalcode 2025-08-11 20:31:45 +07:00
b70c671710 Aplikasi BisaUMKM fix 2025-08-07 10:32:58 +07:00
3ac0461c7c Delete unduh/bisaUMKM.txt 2025-08-07 01:46:47 +07:00
61e8e1fe3c BisaUMKM server 2025-08-07 01:44:51 +07:00
39aa079003 BisaUMKM local 2025-08-07 01:41:43 +07:00
577fb27495 Create bisaUMKM.txt 2025-08-07 01:40:03 +07:00
8b76077a77 Merge remote-tracking branch 'origin/master' 2025-08-07 01:16:45 +07:00
6194dca259 update count product, chat, address 2025-08-07 01:11:55 +07:00
97be7a8f62 Add files via upload
add apps
2025-08-05 16:44:20 +07:00
d6d27e1c61 Update README.md 2025-07-10 15:07:55 +07:00
5992539ef3 Update README.md 2025-07-10 15:05:25 +07:00
c6c1c9f348 Update README.md 2025-07-10 14:57:36 +07:00
7f01914cb0 Create README.md 2025-07-10 14:53:11 +07:00
f43d160fe1 fix bug 2025-07-10 12:10:43 +07:00
bbaf6ed45d fix bug 2025-07-10 12:05:45 +07:00
9299fcbad1 fix bug 2025-07-10 12:04:18 +07:00
8b2a092465 fix bug 2025-07-10 12:00:40 +07:00
adf324e15d fix bug 2025-07-10 11:58:14 +07:00
f7f198e46f update refresh change tab 2025-07-02 08:53:16 +07:00
331410eb98 update province cities subdistricts and villages 2025-07-02 01:40:41 +07:00
db38654159 Merge remote-tracking branch 'origin/master' 2025-06-25 17:25:18 +07:00
2423f45968 Merge pull request #34
store under review
2025-06-23 22:49:05 +07:00
f539bfb9f0 store under review 2025-06-23 22:48:37 +07:00
c2bed56bf5 Merge pull request #33
gracia
2025-06-23 11:11:33 +07:00
0b47d0beb8 store review 2025-06-23 11:11:03 +07:00
d38bdb77dd Merge remote-tracking branch 'origin/master' 2025-06-21 18:13:09 +07:00
019b469556 fix price and chat date 2025-06-21 18:12:54 +07:00
4401daa310 fix search 2025-06-21 18:12:54 +07:00
28b0d5b082 update display category list 2025-06-21 18:12:54 +07:00
b37848e513 update display product list and paid status 2025-06-21 18:12:54 +07:00
6659ba4288 update 2025-06-21 18:12:45 +07:00
88cf5f1457 Merge pull request #32
gracia
2025-06-21 16:56:22 +07:00
e4fb409097 all sells list 2025-06-21 15:16:29 +07:00
3c97b3b3de product change stock and price 2025-06-20 01:50:39 +07:00
107 changed files with 4089 additions and 1433 deletions

3
.gitignore vendored
View File

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

68
README.md Normal file
View File

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

1
app/.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,9 @@ data class AddressResponse(
data class AddressesItem(
@field:SerializedName("village_id")
val villageId: String,
@field:SerializedName("is_store_location")
val isStoreLocation: Boolean,
@ -23,7 +26,7 @@ data class AddressesItem(
val userId: Int,
@field:SerializedName("province_id")
val provinceId: Int,
val provinceId: String,
@field:SerializedName("phone")
val phone: String,
@ -50,5 +53,5 @@ data class AddressesItem(
val longitude: String,
@field:SerializedName("city_id")
val cityId: Int
val cityId: String
)

View File

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

View File

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

View File

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

View File

@ -53,6 +53,8 @@ import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityRespon
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
@ -68,13 +70,14 @@ import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
import okhttp3.MultipartBody
@ -507,4 +510,18 @@ interface ApiService {
@GET("mystore/notification")
suspend fun getNotifStore(
): Response<ListStoreNotifResponse>
@GET("store/reviews")
suspend fun getStoreProductReview(
): Response<ProductReviewResponse>
@GET("subdistrict/{cityId}")
suspend fun getSubdistrict(
@Path("cityId") cityId: String
): Response<SubdistrictResponse>
@GET("villages/{subdistrictId}")
suspend fun getVillages(
@Path("subdistrictId") subdistrictId: String
): Response<VillagesResponse>
}

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.data.repository
import android.util.Log
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
@ -123,6 +124,20 @@ class MyStoreRepository(private val apiService: ApiService) {
}
}
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
return try {
val response = apiService.getStoreProduct()
if (response.isSuccessful) {
response.body()?.products?.filterNotNull() ?: emptyList()
} else {
throw Exception("Failed to fetch store products: ${response.message()}")
}
} catch (e: Exception) {
Log.e("ProductRepository", "Error fetching store products", e)
throw e
}
}
// private fun fetchBalance() {
// showLoading(true)
// lifecycleScope.launch {

View File

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

View File

@ -21,6 +21,8 @@ import com.alya.ecommerce_serang.data.api.response.auth.RegisterStoreResponse
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.utils.FileUtils
@ -68,6 +70,16 @@ class UserRepository(private val apiService: ApiService) {
return if (response.isSuccessful) response.body() else null
}
suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
val response = apiService.getSubdistrict(cityId)
return if (response.isSuccessful) response.body() else null
}
suspend fun getListVillages(subId: String): VillagesResponse? {
val response = apiService.getVillages(subId)
return if (response.isSuccessful) response.body() else null
}
suspend fun registerUser(request: RegisterRequest): RegisterResponse {
val response = apiService.register(request) // API call
@ -87,7 +99,7 @@ class UserRepository(private val apiService: ApiService) {
longitude: String,
street: String,
subdistrict: String,
cityId: Int,
cityId: String,
provinceId: Int,
postalCode: Int,
detail: String,
@ -266,6 +278,11 @@ class UserRepository(private val apiService: ApiService) {
val requestFile = compressedFile.asRequestBody(mimeType.toMediaTypeOrNull())
Log.d(TAG, "$formName compressed size: ${compressedFile.length() / 1024} KB")
val compressedSizeMB = compressedFile.length().toDouble() / (1024 * 1024)
if (compressedSizeMB > 1) {
throw IllegalArgumentException("$formName lebih dari 1 MB setelah kompresi")
}
MultipartBody.Part.createFormData(formName, compressedFile.name, requestFile)
} else {
throw IllegalArgumentException("$formName harus berupa file gambar (JPEG, JPG, atau PNG)")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package com.alya.ecommerce_serang.ui.cart
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
@ -145,7 +146,9 @@ class CartActivity : AppCompatActivity() {
private fun observeViewModel() {
viewModel.cartItems.observe(this) { cartItems ->
if (cartItems.isNullOrEmpty()) {
binding.emptyCart.visibility = View.VISIBLE
showEmptyState(true)
} else {
showEmptyState(false)
storeAdapter.submitList(cartItems)
@ -153,7 +156,8 @@ class CartActivity : AppCompatActivity() {
}
viewModel.isLoading.observe(this) { isLoading ->
// Show/hide loading indicator if needed
binding.progressBarCart?.visibility = if (isLoading) View.VISIBLE else View.GONE
Log.d("CartActivity", "Loading state: $isLoading")
}
viewModel.errorMessage.observe(this) { errorMessage ->

View File

@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
@ -129,6 +130,7 @@ class ChatActivity : AppCompatActivity() {
return
}
// set up data toko
binding.tvStoreName.text = storeName
val fullImageUrl = when (val img = storeImg) {
is String -> {
@ -142,7 +144,7 @@ class ChatActivity : AppCompatActivity() {
.placeholder(R.drawable.placeholder_image)
.into(binding.imgProfile)
// Set chat parameters to ViewModel
// Set chat parameter to send to ViewModel with product
viewModel.setChatParameters(
storeId = storeId,
productId = productId,
@ -159,16 +161,17 @@ class ChatActivity : AppCompatActivity() {
}
// Setup UI components
// rv isi chat
setupRecyclerView()
setupWindowInsets()
setupListeners()
setupTypingIndicator()
// observe listener from viewmodel
observeViewModel()
// If opened from ChatListFragment with a valid chatRoomId
if (chatRoomId > 0) {
// Directly set the chatRoomId and load chat history
viewModel._chatRoomId.value = chatRoomId
viewModel.setChatRoomId(chatRoomId)
}
}
@ -402,7 +405,8 @@ class ChatActivity : AppCompatActivity() {
}
})
viewModel.state.observe(this, Observer { state ->
lifecycleScope.launchWhenStarted {
viewModel.state.collect() { state ->
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
// Update messages
@ -431,6 +435,7 @@ class ChatActivity : AppCompatActivity() {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image
}
@ -463,7 +468,8 @@ class ChatActivity : AppCompatActivity() {
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
viewModel.clearError()
}
})
}
}
}
private fun updateInputHint(state: ChatUiState) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
@ -110,11 +111,6 @@ class CheckoutActivity : AppCompatActivity() {
finish()
}
}
// viewModel.getPaymentMethods { paymentMethods ->
// // Logging is just for debugging
// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
// }
}
private fun setupToolbar() {
@ -165,7 +161,7 @@ class CheckoutActivity : AppCompatActivity() {
// Observe loading state
viewModel.isLoading.observe(this) { isLoading ->
binding.btnPay.isEnabled = !isLoading
// Show/hide loading indicator if you have one
}
// Observe error messages
@ -273,10 +269,14 @@ class CheckoutActivity : AppCompatActivity() {
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
// Display shipping name and service in one line
binding.cardShipment.visibility = View.VISIBLE
binding.tvCourierName.text = "$shipName $shipService"
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
binding.rbJne.isChecked = true
} else {
binding.cardShipment.visibility = View.GONE
}
}

View File

@ -289,7 +289,7 @@ class AddAddressActivity : AppCompatActivity() {
val isStoreLocation = false
val provinceId = viewModel.selectedProvinceId
val cityId = viewModel.selectedCityId
val cityId = viewModel.selectedCityId.toString()
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
@ -333,18 +333,19 @@ class AddAddressActivity : AppCompatActivity() {
// Create request with all fields
val request = CreateAddressRequest(
userId = userId,
lat = latitude!!, // Safe to use !! as we've checked above
long = longitude!!,
street = street,
subDistrict = subDistrict,
cityId = cityId,
cityId = cityId, // ⚠️ Make sure this is Int
provId = provinceId,
postCode = postalCode,
idVillage = "", // Or provide a real ID if needed
detailAddress = street,
userId = userId,
isStoreLocation = false,
recipient = recipient,
phone = phone,
isStoreLocation = isStoreLocation
phone = phone
)
Log.d(TAG, "Form validation successful, submitting address: $request")

View File

@ -36,8 +36,8 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
get() = savedStateHandle.get<Int>("selectedProvinceId")
set(value) { savedStateHandle["selectedProvinceId"] = value }
var selectedCityId: Int?
get() = savedStateHandle.get<Int>("selectedCityId")
var selectedCityId: String?
get() = savedStateHandle.get<String>("selectedCityId")
set(value) { savedStateHandle["selectedCityId"] = value }
init {
@ -129,7 +129,7 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
selectedProvinceId = id
}
fun setSelectedCityId(id: Int) {
fun updateSelectedCityId(id: String) {
selectedCityId = id
}

View File

@ -5,6 +5,8 @@ import android.util.Log
import android.widget.ArrayAdapter
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
// UI adapters and helpers
class ProvinceAdapter(
@ -12,6 +14,7 @@ class ProvinceAdapter(
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
//call from endpoint
private val provinces = ArrayList<ProvincesItem>()
fun updateData(newProvinces: List<ProvincesItem>) {
@ -46,7 +49,52 @@ class CityAdapter(
notifyDataSetChanged()
}
fun getCityId(position: Int): Int? {
return cities.getOrNull(position)?.cityId?.toIntOrNull()
fun getCityId(position: Int): String? {
return cities.getOrNull(position)?.cityId?.toString()
}
}
class SubdsitrictAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val cities = ArrayList<SubdistrictsItem>()
fun updateData(newCities: List<SubdistrictsItem>) {
cities.clear()
cities.addAll(newCities)
clear()
addAll(cities.map { it.subdistrictName })
notifyDataSetChanged()
}
fun getSubdistrictId(position: Int): String? {
return cities.getOrNull(position)?.subdistrictId?.toString()
}
}
class VillagesAdapter(
context: Context,
resource: Int = android.R.layout.simple_dropdown_item_1line
) : ArrayAdapter<String>(context, resource, ArrayList()) {
private val villages = ArrayList<VillagesItem>()
fun updateData(newCities: List<VillagesItem>) {
villages.clear()
villages.addAll(newCities)
clear()
addAll(villages.map { it.villageName })
notifyDataSetChanged()
}
fun getVillageId(position: Int): String? {
return villages.getOrNull(position)?.villageId?.toString()
}
fun getPostalCode(position: Int): String?{
return villages.getOrNull(position)?.postalCode
}
}

View File

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

View File

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

View File

@ -195,7 +195,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
// viewModel.refreshOrders()
}
}
// deadlineDate.apply {
@ -213,14 +213,15 @@ class OrderHistoryAdapter(
visibility = View.VISIBLE
text = itemView.context.getString(R.string.dl_processed)
}
btnLeft.apply {
visibility = View.VISIBLE
text = itemView.context.getString(R.string.canceled_order_btn)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
}
}
// gabisa complaint
// btnLeft.apply {
// visibility = View.VISIBLE
// text = itemView.context.getString(R.string.canceled_order_btn)
// setOnClickListener {
// showCancelOrderDialog(order.orderId.toString())
// viewModel.refreshOrders()
// }
// }
}
"shipped" -> {
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
@ -237,7 +238,7 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.claim_complaint)
setOnClickListener {
showCancelOrderDialog(order.orderId.toString())
viewModel.refreshOrders()
// viewModel.refreshOrders()
}
}
btnRight.apply {
@ -248,7 +249,7 @@ class OrderHistoryAdapter(
// Call ViewModel
viewModel.confirmOrderCompleted(order.orderId, "completed")
viewModel.refreshOrders()
// viewModel.refreshOrders()
}
@ -268,13 +269,21 @@ class OrderHistoryAdapter(
text = itemView.context.getString(R.string.dl_shipped)
}
btnRight.apply {
val checkReview = order.orderItems[0].reviewId
if (checkReview > 0){
visibility = View.VISIBLE
text = itemView.context.getString(R.string.add_review)
setOnClickListener {
addReviewProduct(order)
viewModel.refreshOrders()
// viewModel.refreshOrders()
// Handle click event
}
} else {
visibility = View.GONE
}
}
deadlineDate.apply {
visibility = View.VISIBLE
@ -518,7 +527,7 @@ class OrderHistoryAdapter(
}
}
// Create and show the bottom sheet using the obtained FragmentManager
// cancel sebelum bayar
val bottomSheet = CancelOrderBottomSheet(
orderId = orderId,
onOrderCancelled = {
@ -531,6 +540,7 @@ class OrderHistoryAdapter(
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
}
// tambah review / ulasan
private fun addReviewProduct(order: OrdersItem) {
// Use ViewModel to fetch order details
viewModel.getOrderDetails(order.orderId)
@ -550,7 +560,7 @@ class OrderHistoryAdapter(
}
}
// Observe the order details result
// Observe order items
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
if (orderItems != null && orderItems.isNotEmpty()) {
// For single item review

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
package com.alya.ecommerce_serang.ui.auth
package com.alya.ecommerce_serang.ui.profile.mystore
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@ -21,11 +19,12 @@ import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.Result
@ -35,6 +34,7 @@ import com.alya.ecommerce_serang.ui.order.address.CityAdapter
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
class RegisterStoreActivity : AppCompatActivity() {
@ -48,15 +48,13 @@ class RegisterStoreActivity : AppCompatActivity() {
private val PICK_KTP_REQUEST = 1002
private val PICK_NPWP_REQUEST = 1003
private val PICK_NIB_REQUEST = 1004
private val PICK_PERSETUJUAN_REQUEST = 1005
private val PICK_QRIS_REQUEST = 1006
// Location request code
private val LOCATION_PERMISSION_REQUEST = 2001
private val viewModel: RegisterStoreViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val apiService = ApiConfig.Companion.getApiService(sessionManager)
val orderRepository = UserRepository(apiService)
RegisterStoreViewModel(orderRepository)
}
@ -84,6 +82,8 @@ class RegisterStoreActivity : AppCompatActivity() {
windowInsets
}
setupHeader()
provinceAdapter = ProvinceAdapter(this)
cityAdapter = CityAdapter(this)
Log.d(TAG, "onCreate: Adapters initialized")
@ -99,8 +99,12 @@ class RegisterStoreActivity : AppCompatActivity() {
setupObservers()
Log.d(TAG, "onCreate: Observers setup completed")
setupMap()
Log.d(TAG, "onCreate: Map setup completed")
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
setupDocumentUploads()
Log.d(TAG, "onCreate: Document uploads setup completed")
@ -114,6 +118,9 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "onCreate: Fetching provinces from API")
viewModel.getProvinces()
viewModel.provinceId.observe(this) { validateRequiredFields() }
viewModel.cityId.observe(this) { validateRequiredFields() }
viewModel.storeTypeId.observe(this) { validateRequiredFields() }
// Setup register button
binding.btnRegister.setOnClickListener {
@ -130,6 +137,46 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "onCreate: RegisterStoreActivity setup completed")
}
private fun setupHeader() {
binding.header.main.background = ContextCompat.getColor(this, R.color.blue_500).toDrawable()
binding.header.headerTitle.visibility = View.GONE
binding.header.headerLeftIcon.setColorFilter(
ContextCompat.getColor(this, R.color.white),
android.graphics.PorterDuff.Mode.SRC_IN
)
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
private fun validateRequiredFields() {
val isFormValid = !viewModel.storeName.value.isNullOrBlank() &&
!viewModel.street.value.isNullOrBlank() &&
(viewModel.postalCode.value ?: 0) > 0 &&
!viewModel.subdistrict.value.isNullOrBlank() &&
!viewModel.bankName.value.isNullOrBlank() &&
(viewModel.bankNumber.value ?: 0) > 0 &&
(viewModel.provinceId.value ?: 0) > 0 &&
!viewModel.cityId.value.isNullOrBlank() &&
(viewModel.storeTypeId.value ?: 0) > 0 &&
viewModel.ktpUri != null &&
viewModel.nibUri != null &&
viewModel.npwpUri != null &&
viewModel.selectedCouriers.isNotEmpty() &&
!viewModel.accountName.value.isNullOrBlank()
binding.btnRegister.isEnabled = true
if (isFormValid) {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_active)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.white))
} else {
binding.btnRegister.setBackgroundResource(R.drawable.bg_button_disabled)
binding.btnRegister.setTextColor(ContextCompat.getColor(this, R.color.black_300))
}
}
private fun setupObservers() {
Log.d(TAG, "setupObservers: Setting up LiveData observers")
@ -138,12 +185,12 @@ class RegisterStoreActivity : AppCompatActivity() {
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading provinces...")
binding.provinceProgressBar?.visibility = View.VISIBLE
binding.provinceProgressBar.visibility = View.VISIBLE
binding.spinnerProvince.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Provinces loaded successfully: ${state.data.size} provinces")
binding.provinceProgressBar?.visibility = View.GONE
binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
// Update adapter with data
@ -151,7 +198,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading provinces: ${state.exception.message}")
binding.provinceProgressBar?.visibility = View.GONE
binding.provinceProgressBar.visibility = View.GONE
binding.spinnerProvince.isEnabled = true
}
}
@ -162,12 +209,12 @@ class RegisterStoreActivity : AppCompatActivity() {
when (state) {
is Result.Loading -> {
Log.d(TAG, "setupObservers: Loading cities...")
binding.cityProgressBar?.visibility = View.VISIBLE
binding.cityProgressBar.visibility = View.VISIBLE
binding.spinnerCity.isEnabled = false
}
is Result.Success -> {
Log.d(TAG, "setupObservers: Cities loaded successfully: ${state.data.size} cities")
binding.cityProgressBar?.visibility = View.GONE
binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true
// Update adapter with data
@ -175,7 +222,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
is Result.Error -> {
Log.e(TAG, "setupObservers: Error loading cities: ${state.exception.message}")
binding.cityProgressBar?.visibility = View.GONE
binding.cityProgressBar.visibility = View.GONE
binding.spinnerCity.isEnabled = true
}
}
@ -214,11 +261,11 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "setupStoreTypesObserver: Loading store types...")
// Show loading indicator for store types spinner
binding.spinnerStoreType.isEnabled = false
binding.storeTypeProgressBar?.visibility = View.VISIBLE
binding.storeTypeProgressBar.visibility = View.VISIBLE
} else {
Log.d(TAG, "setupStoreTypesObserver: Store types loading completed")
binding.spinnerStoreType.isEnabled = true
binding.storeTypeProgressBar?.visibility = View.GONE
binding.storeTypeProgressBar.visibility = View.GONE
}
}
@ -309,7 +356,7 @@ class RegisterStoreActivity : AppCompatActivity() {
}
// Hide progress bar after setup
binding.storeTypeProgressBar?.visibility = View.GONE
binding.storeTypeProgressBar.visibility = View.GONE
Log.d(TAG, "setupStoreTypeSpinner: Store type spinner setup completed")
}
@ -398,23 +445,11 @@ class RegisterStoreActivity : AppCompatActivity() {
}
// NPWP
binding.containerNpwp?.setOnClickListener {
binding.containerNpwp.setOnClickListener {
Log.d(TAG, "NPWP container clicked, picking image")
pickImage(PICK_NPWP_REQUEST)
}
// SPPIRT
binding.containerSppirt.setOnClickListener {
Log.d(TAG, "SPPIRT container clicked, picking document")
pickDocument(PICK_PERSETUJUAN_REQUEST)
}
// Halal
binding.containerHalal.setOnClickListener {
Log.d(TAG, "Halal container clicked, picking document")
pickDocument(PICK_QRIS_REQUEST)
}
Log.d(TAG, "setupDocumentUploads: Document upload buttons setup completed")
}
@ -442,16 +477,16 @@ class RegisterStoreActivity : AppCompatActivity() {
handleCourierSelection("jne", isChecked)
}
binding.checkboxJnt.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "JNT checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
binding.checkboxPos.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "POS checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("pos", isChecked)
}
binding.checkboxTiki.setOnCheckedChangeListener { _, isChecked ->
Log.d(TAG, "TIKI checkbox ${if (isChecked) "checked" else "unchecked"}")
handleCourierSelection("tiki", isChecked)
}
Log.d(TAG, "setupCourierSelection: Courier checkboxes setup completed")
}
@ -465,46 +500,47 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.selectedCouriers.remove(courier)
Log.d(TAG, "handleCourierSelection: Removed courier: $courier. Current couriers: ${viewModel.selectedCouriers}")
}
validateRequiredFields()
}
private fun setupMap() {
Log.d(TAG, "setupMap: Setting up map container")
// This would typically integrate with Google Maps SDK
// For simplicity, we're just using a placeholder
binding.mapContainer.setOnClickListener {
Log.d(TAG, "Map container clicked, checking location permission")
// Request location permission if not granted
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "Location permission not granted, requesting permission")
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
LOCATION_PERMISSION_REQUEST
)
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location permission granted, setting default location")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
} else {
Log.d(TAG, "Location permission already granted, setting location")
// Show map selection UI
// This would typically launch Maps UI for location selection
// For now, we'll just set some dummy coordinates
viewModel.latitude.value = "-6.2088"
viewModel.longitude.value = "106.8456"
Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
}
}
Log.d(TAG, "setupMap: Map container setup completed")
}
// private fun setupMap() {
// Log.d(TAG, "setupMap: Setting up map container")
// // This would typically integrate with Google Maps SDK
// // For simplicity, we're just using a placeholder
// binding.mapContainer.setOnClickListener {
// Log.d(TAG, "Map container clicked, checking location permission")
// // Request location permission if not granted
// if (ContextCompat.checkSelfPermission(
// this,
// Manifest.permission.ACCESS_FINE_LOCATION
// ) != PackageManager.PERMISSION_GRANTED
// ) {
// Log.d(TAG, "Location permission not granted, requesting permission")
// ActivityCompat.requestPermissions(
// this,
// arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
// LOCATION_PERMISSION_REQUEST
// )
// viewModel.latitude.value = "-6.2088"
// viewModel.longitude.value = "106.8456"
// Log.d(TAG, "Location permission granted, setting default location")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// Log.d(TAG, "Default location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// } else {
// Log.d(TAG, "Location permission already granted, setting location")
// // Show map selection UI
// // This would typically launch Maps UI for location selection
// // For now, we'll just set some dummy coordinates
// viewModel.latitude.value = "-6.2088"
// viewModel.longitude.value = "106.8456"
// Log.d(TAG, "Location set - Lat: ${viewModel.latitude.value}, Long: ${viewModel.longitude.value}")
// Toast.makeText(this, "Lokasi dipilih", Toast.LENGTH_SHORT).show()
// }
// }
//
// Log.d(TAG, "setupMap: Map container setup completed")
// }
private fun setupDataBinding() {
Log.d(TAG, "setupDataBinding: Setting up two-way data binding for text fields")
@ -516,6 +552,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.storeName.value = s.toString()
Log.d(TAG, "Store name updated: ${s.toString()}")
validateRequiredFields()
}
})
@ -534,6 +571,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.street.value = s.toString()
Log.d(TAG, "Street address updated: ${s.toString()}")
validateRequiredFields()
}
})
@ -547,6 +585,7 @@ class RegisterStoreActivity : AppCompatActivity() {
} catch (e: NumberFormatException) {
// Handle invalid input
Log.e(TAG, "Invalid postal code input: ${s.toString()}, error: $e")
validateRequiredFields()
}
}
})
@ -578,6 +617,7 @@ class RegisterStoreActivity : AppCompatActivity() {
viewModel.bankNumber.value = 0 // or 0
Log.d(TAG, "Bank number set to default: 0")
}
validateRequiredFields()
}
})
@ -587,6 +627,7 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.subdistrict.value = s.toString()
Log.d(TAG, "Subdistrict updated: ${s.toString()}")
validateRequiredFields()
}
})
@ -596,16 +637,28 @@ class RegisterStoreActivity : AppCompatActivity() {
override fun afterTextChanged(s: Editable?) {
viewModel.bankName.value = s.toString()
Log.d(TAG, "Bank name updated: ${s.toString()}")
validateRequiredFields()
}
})
binding.etAccountName.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
viewModel.accountName.value = s.toString()
Log.d(TAG, "Account Name updated: ${s.toString()}")
validateRequiredFields()
}
})
Log.d(TAG, "setupDataBinding: Text field data binding setup completed")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d(TAG, "onActivityResult: Request code: $requestCode, Result code: $resultCode")
if (resultCode == Activity.RESULT_OK && data != null) {
if (resultCode == RESULT_OK && data != null) {
val uri = data.data
Log.d(TAG, "onActivityResult: URI received: $uri")
when (requestCode) {
@ -618,26 +671,19 @@ class RegisterStoreActivity : AppCompatActivity() {
Log.d(TAG, "KTP image selected")
viewModel.ktpUri = uri
updateImagePreview(uri, binding.imgKtp, binding.layoutUploadKtp)
validateRequiredFields()
}
PICK_NPWP_REQUEST -> {
Log.d(TAG, "NPWP document selected")
viewModel.npwpUri = uri
updateDocumentPreview(binding.layoutUploadNpwp)
validateRequiredFields()
}
PICK_NIB_REQUEST -> {
Log.d(TAG, "NIB document selected")
viewModel.nibUri = uri
updateDocumentPreview(binding.layoutUploadNib)
}
PICK_PERSETUJUAN_REQUEST -> {
Log.d(TAG, "SPPIRT document selected")
viewModel.persetujuanUri = uri
updateDocumentPreview(binding.layoutUploadSppirt)
}
PICK_QRIS_REQUEST -> {
Log.d(TAG, "Halal document selected")
viewModel.qrisUri = uri
updateDocumentPreview(binding.layoutUploadHalal)
validateRequiredFields()
}
else -> {
Log.w(TAG, "Unknown request code: $requestCode")

View File

@ -0,0 +1,25 @@
package com.alya.ecommerce_serang.ui.profile.mystore
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.databinding.ActivityStoreOnReviewBinding
class StoreOnReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityStoreOnReviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityStoreOnReviewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.header.headerTitle.text = "Verifikasi Pengajuan Toko"
binding.header.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
finish()
}
}
}

View File

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

View File

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

View File

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

View File

@ -12,30 +12,29 @@ import android.view.View
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.alya.ecommerce_serang.R
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.CategoryItem
import com.alya.ecommerce_serang.data.api.dto.Preorder
import com.alya.ecommerce_serang.data.api.dto.Wholesale
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
import com.bumptech.glide.Glide
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
import java.io.FileOutputStream
import kotlin.getValue
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.data.api.dto.Wholesale
class DetailStoreProductActivity : AppCompatActivity() {
@ -93,7 +92,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
val isEditing = intent.getBooleanExtra("is_editing", false)
productId = intent.getIntExtra("product_id", -1)
binding.header.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
binding.headerStoreProduct.headerTitle.text = if (isEditing) "Ubah Produk" else "Tambah Produk"
if (isEditing && productId != null && productId != -1) {
viewModel.loadProductDetail(productId!!)
@ -140,7 +139,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
}
}
binding.header.headerLeftIcon.setOnClickListener {
binding.headerStoreProduct.headerLeftIcon.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ class SellsAdapter(
Log.d("SellsAdapter", "=== ViewHolder.bind() called ===")
Log.d("SellsAdapter", "Binding order: ${order.orderId} with status: ${order.status}")
val actualStatus = if (fragmentStatus == "all") order.status ?: "" else fragmentStatus
val actualStatus = if (fragmentStatus == "all") order.displayStatus ?: "" else fragmentStatus
adjustDisplay(actualStatus, order)
tvSellsNumber.text = "No. Pesanan: ${order.orderId}"

View File

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

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
import com.alya.ecommerce_serang.data.api.dto.Store
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
@ -38,6 +39,9 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
private val _balanceResult = MutableLiveData<Result<StoreResponse>>()
val balanceResult: LiveData<Result<StoreResponse>> get() = _balanceResult
private val _productList = MutableLiveData<Result<List<ProductsItem>>>()
val productList: LiveData<Result<List<ProductsItem>>> get() = _productList
fun loadMyStore(){
viewModelScope.launch {
when (val result = repository.fetchMyStoreProfile()){
@ -158,6 +162,18 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
}
}
fun loadMyStoreProducts() {
viewModelScope.launch {
_productList.value = Result.Loading
try {
val result = repository.fetchMyStoreProducts()
_productList.value = Result.Success(result)
} catch (e: Exception) {
_productList.value = Result.Error(e)
}
}
}
private fun String.toRequestBody(): RequestBody =
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
}

View File

@ -111,9 +111,9 @@ class ProductViewModel(private val repository: ProductRepository) : ViewModel()
fun updateProduct(
productId: Int?,
data: Map<String, RequestBody>,
image: MultipartBody.Part?,
halal: MultipartBody.Part?,
sppirt: MultipartBody.Part?
image: MultipartBody.Part? = null,
halal: MultipartBody.Part? = null,
sppirt: MultipartBody.Part? = null
) {
viewModelScope.launch {
_productUpdateResult.postValue(Result.Loading)

View File

@ -1,4 +1,4 @@
package com.alya.ecommerce_serang.ui.auth
package com.alya.ecommerce_serang.utils.viewmodel
import android.content.Context
import android.net.Uri
@ -21,8 +21,8 @@ class RegisterStoreViewModel(
) : ViewModel() {
// LiveData for UI state
private val _registerState = MutableLiveData<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>>()
val registerState: LiveData<com.alya.ecommerce_serang.data.repository.Result<RegisterStoreResponse>> = _registerState
private val _registerState = MutableLiveData<Result<RegisterStoreResponse>>()
val registerState: LiveData<Result<RegisterStoreResponse>> = _registerState
private val _storeTypes = MutableLiveData<List<StoreTypesItem>>()
val storeTypes: LiveData<List<StoreTypesItem>> = _storeTypes
@ -42,7 +42,7 @@ class RegisterStoreViewModel(
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
var selectedCityId: String? = null
// Form fields
val storeName = MutableLiveData<String>()
@ -52,7 +52,7 @@ class RegisterStoreViewModel(
val longitude = MutableLiveData<String>()
val street = MutableLiveData<String>()
val subdistrict = MutableLiveData<String>()
val cityId = MutableLiveData<Int>()
val cityId = MutableLiveData<String>()
val provinceId = MutableLiveData<Int>()
val postalCode = MutableLiveData<Int>()
val addressDetail = MutableLiveData<String>()
@ -72,47 +72,89 @@ class RegisterStoreViewModel(
val selectedCouriers = mutableListOf<String>()
fun registerStore(context: Context) {
Log.d(TAG, "Starting registerStore()")
val allowedFileTypes = Regex("^(jpeg|jpg|png|pdf)$", RegexOption.IGNORE_CASE)
// Check each file if present
fun logFileInfo(label: String, uri: Uri?) {
if (uri == null) {
Log.d(TAG, "$label URI: null")
return
}
Log.d(TAG, "$label URI: $uri")
try {
val fileSizeBytes = context.contentResolver.openFileDescriptor(uri, "r")?.use {
it.statSize
} ?: -1
Log.d(TAG, "$label original size: ${fileSizeBytes / 1024} KB")
} catch (e: Exception) {
Log.e(TAG, "Error getting size for $label", e)
}
}
// Log all file info before validation
logFileInfo("Store Image", storeImageUri)
logFileInfo("KTP", ktpUri)
logFileInfo("NPWP", npwpUri)
logFileInfo("NIB", nibUri)
logFileInfo("Persetujuan", persetujuanUri)
logFileInfo("QRIS", qrisUri)
// Check file types
if (storeImageUri != null && !ImageUtils.isAllowedFileType(context, storeImageUri, allowedFileTypes)) {
_errorMessage.value = "Foto toko harus berupa file JPEG, JPG, atau PNG"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for store image")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (ktpUri != null && !ImageUtils.isAllowedFileType(context, ktpUri, allowedFileTypes)) {
_errorMessage.value = "KTP harus berupa file JPEG, JPG, atau PNG"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for KTP")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (npwpUri != null && !ImageUtils.isAllowedFileType(context, npwpUri, allowedFileTypes)) {
_errorMessage.value = "NPWP harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for NPWP")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (nibUri != null && !ImageUtils.isAllowedFileType(context, nibUri, allowedFileTypes)) {
_errorMessage.value = "NIB harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for NIB")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (persetujuanUri != null && !ImageUtils.isAllowedFileType(context, persetujuanUri, allowedFileTypes)) {
_errorMessage.value = "Persetujuan harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for Persetujuan")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
if (qrisUri != null && !ImageUtils.isAllowedFileType(context, qrisUri, allowedFileTypes)) {
_errorMessage.value = "QRIS harus berupa file JPEG, JPG, PNG, atau PDF"
Log.e(TAG, _errorMessage.value ?: "Invalid file type for QRIS")
_registerState.value = Result.Error(Exception(_errorMessage.value ?: "Invalid file type"))
return
}
Log.d(TAG, "File type checks passed. Starting repository.registerStoreUser() call.")
viewModelScope.launch {
try {
_registerState.value = Result.Loading
Log.d(TAG, "Register store request payload: " +
"storeName=${storeName.value}, storeTypeId=${storeTypeId.value}, " +
"lat=${latitude.value}, long=${longitude.value}, " +
"street=${street.value}, subdistrict=${subdistrict.value}, " +
"cityId=${cityId.value}, provinceId=${provinceId.value}, postalCode=${postalCode.value}, " +
"bankName=${bankName.value}, bankNum=${bankNumber.value}, accountName=${accountName.value}, " +
"selectedCouriers=$selectedCouriers")
val result = repository.registerStoreUser(
context = context,
@ -122,7 +164,7 @@ class RegisterStoreViewModel(
longitude = longitude.value ?: "",
street = street.value ?: "",
subdistrict = subdistrict.value ?: "",
cityId = cityId.value ?: 0,
cityId = cityId.value ?: "",
provinceId = provinceId.value ?: 0,
postalCode = postalCode.value ?: 0,
detail = addressDetail.value ?: "",
@ -139,25 +181,15 @@ class RegisterStoreViewModel(
accountName = accountName.value ?: ""
)
Log.d(TAG, "Repository returned result: $result")
_registerState.value = result
} catch (e: Exception) {
_registerState.value = com.alya.ecommerce_serang.data.repository.Result.Error(e)
Log.e(TAG, "Exception during registerStore", e)
_registerState.value = Result.Error(e)
}
}
}
// // Helper function to convert Uri to File
// private fun getFileFromUri(context: Context, uri: Uri): File {
// val inputStream = context.contentResolver.openInputStream(uri)
// val tempFile = File(context.cacheDir, "temp_file_${System.currentTimeMillis()}")
// inputStream?.use { input ->
// tempFile.outputStream().use { output ->
// input.copyTo(output)
// }
// }
// return tempFile
// }
fun validateForm(): Boolean {
// Implement form validation logic
return !(storeName.value.isNullOrEmpty() ||
@ -174,8 +206,6 @@ class RegisterStoreViewModel(
nibUri == null)
}
// Function to fetch store types
fun fetchStoreTypes() {
_isLoadingType.value = true

View File

@ -16,6 +16,8 @@ import com.alya.ecommerce_serang.data.api.response.auth.User
import com.alya.ecommerce_serang.data.api.response.auth.VerifRegisterResponse
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.OrderRepository
@ -62,17 +64,29 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
private val _registeredUser = MutableLiveData<User>()
val registeredUser: LiveData<User> = _registeredUser
private val _toastMessage = MutableLiveData<com.alya.ecommerce_serang.utils.viewmodel.Event<String>>()
val toastMessage: LiveData<com.alya.ecommerce_serang.utils.viewmodel.Event<String>> = _toastMessage
// For address data
var selectedProvinceId: Int? = null
var selectedCityId: Int? = null
var selectedCityId: String? = null
var selectedSubdistrict: String? = null
var selectedVillages: String? = null
var selectedPostalCode: String? = null
// For provinces and cities
// For provinces and cities using raja ongkir
private val _provincesState = MutableLiveData<ViewState<List<ProvincesItem>>>()
val provincesState: LiveData<ViewState<List<ProvincesItem>>> = _provincesState
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
private val _subdistrictState = MutableLiveData<ViewState<List<SubdistrictsItem>>>()
val subdistrictState: LiveData<ViewState<List<SubdistrictsItem>>> = _subdistrictState
private val _villagesState = MutableLiveData<ViewState<List<VillagesItem>>>()
val villagesState: LiveData<ViewState<List<VillagesItem>>> = _villagesState
// For address submission
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
val addressSubmissionState: LiveData<ViewState<String>> = _addressSubmissionState
@ -213,16 +227,23 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
Log.d("RegisterViewModel", "OTP Response: ${response.available}")
_checkValue.value = Result.Success(response.available)// Store the message for UI feedback
val msg = if (response.available)
"${request.fieldRegis.capitalize()} dapat digunakan"
else
"${request.fieldRegis.capitalize()} sudah terdaftar"
_toastMessage.value = Event(msg)
} catch (exception: Exception) {
// Handle any errors and update state
_checkValue.value = Result.Error(exception)
_toastMessage.value = Event("Gagal memeriksa ${request.fieldRegis}")
// Log the error for debugging
Log.e("RegisterViewModel", "Error:", exception)
}
}
}
//using raja ongkir
fun getProvinces() {
_provincesState.value = ViewState.Loading
viewModelScope.launch {
@ -242,6 +263,7 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
}
}
//kota pake raja ongkir
fun getCities(provinceId: Int) {
_citiesState.value = ViewState.Loading
viewModelScope.launch {
@ -263,14 +285,64 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
}
}
fun getSubdistrict(cityId: String) {
_subdistrictState.value = ViewState.Loading
viewModelScope.launch {
try {
selectedSubdistrict = cityId
val result = repository.getListSubdistrict(cityId)
result?.let {
_subdistrictState.postValue(ViewState.Success(it.subdistricts))
Log.d(TAG, "Cities loaded for province $cityId: ${it.subdistricts.size}")
} ?: run {
_subdistrictState.postValue(ViewState.Error("Failed to load cities"))
Log.e(TAG, "City result was null for province $cityId")
}
} catch (e: Exception) {
_subdistrictState.postValue(ViewState.Error(e.message ?: "Error loading cities"))
Log.e(TAG, "Error fetching cities for province $cityId", e)
}
}
}
fun getVillages(subdistrictId: String) {
_villagesState.value = ViewState.Loading
viewModelScope.launch {
try {
selectedVillages = subdistrictId
val result = repository.getListVillages(subdistrictId)
result?.let {
_villagesState.postValue(ViewState.Success(it.villages))
Log.d(TAG, "Cities loaded for province $subdistrictId: ${it.villages.size}")
} ?: run {
_villagesState.postValue(ViewState.Error("Failed to load cities"))
Log.e(TAG, "City result was null for province $subdistrictId")
}
} catch (e: Exception) {
_villagesState.postValue(ViewState.Error(e.message ?: "Error loading cities"))
Log.e(TAG, "Error fetching cities for province $subdistrictId", e)
}
}
}
fun setSelectedProvinceId(id: Int) {
selectedProvinceId = id
}
fun setSelectedCityId(id: Int) {
fun updateSelectedCityId(id: String) {
selectedCityId = id
}
fun updateSelectedSubdistrict(id: String){
selectedSubdistrict = id
}
fun updateSelectedVillages(id: String){
selectedVillages = id
}
fun addAddress(request: CreateAddressRequest) {
Log.d(TAG, "Starting address submission process")
_addressSubmissionState.value = ViewState.Loading
@ -314,5 +386,9 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
private const val TAG = "RegisterViewModel"
}
//require auth
}
class Event<out T>(private val data: T) {
private var handled = false
fun getContentIfNotHandled(): T? = if (handled) null else { handled = true; data }
}

View File

@ -1,7 +1,71 @@
package com.alya.ecommerce_serang.utils.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.data.api.dto.ReviewsItem
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.ReviewRepository
import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.launch
import kotlin.getOrThrow
class ReviewViewModel : ViewModel() {
// TODO: Implement the ViewModel
class ReviewViewModel(private val repository: ReviewRepository) : ViewModel() {
private val _review = MutableLiveData<ViewState<List<ReviewsItem>>>()
val review: LiveData<ViewState<List<ReviewsItem>>> = _review
private val _averageScore = MutableLiveData<String>()
val averageScore: LiveData<String> = _averageScore
private val _totalReview = MutableLiveData<Int>()
val totalReview: LiveData<Int> = _totalReview
private val _totalReviewWithDesc = MutableLiveData<Int>()
val totalReviewWithDesc: LiveData<Int> = _totalReviewWithDesc
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val productImageCache = mutableMapOf<Int, String?>()
fun getReview(score: String) {
_review.value = ViewState.Loading
viewModelScope.launch {
try {
val response = repository.getReviewList(score)
if (response is Result.Success) {
val reviews = response.data.reviews?.filterNotNull().orEmpty()
_review.value = ViewState.Success(reviews)
if (score == "all") {
val avg = if (reviews.isNotEmpty()) {
reviews.mapNotNull { it.rating }.average()
} else 0.0
_averageScore.value = String.format("%.1f", avg)
_totalReview.value = reviews.size
_totalReviewWithDesc.value = reviews.count { !it.reviewText.isNullOrBlank() }
}
} else if (response is Result.Error) {
_review.value = ViewState.Error(response.exception.message ?: "Gagal memuat ulasan")
}
} catch (e: Exception) {
_review.value = ViewState.Error(e.message ?: "Terjadi kesalahan")
}
}
}
suspend fun getProductImage(productId: Int): String? {
if (productImageCache.containsKey(productId)) {
return productImageCache[productId]
}
val result = repository.getProductDetail(productId)
val imageUrl = if (result?.product?.image?.startsWith("/") == true) {
BASE_URL + result.product.image.removePrefix("/")
} else result?.product?.image
productImageCache[productId] = imageUrl.toString()
return imageUrl.toString()
}
}

View File

@ -14,7 +14,12 @@ import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmati
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.SellsRepository
import com.alya.ecommerce_serang.ui.order.address.ViewState
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Locale
class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
@ -59,6 +64,52 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
Log.d(TAG, "Coroutine launched successfully")
try {
if(status == "all") {
Log.d(TAG, "Status is 'all', calling repository.getSellList()")
val allStatuses = listOf("pending", "unpaid", "processed", "shipped")
val allSells = mutableListOf<OrdersItem>()
coroutineScope {
val deferreds = allStatuses.map { status ->
async {
when (val result = repository.getSellList(status)) {
is Result.Success -> {
result.data.orders.onEach { it.displayStatus = status }
}
is Result.Error -> {
Log.e(
TAG,
"Error loading orders for status $status",
result.exception
)
emptyList<OrdersItem>()
}
is Result.Loading -> emptyList<OrdersItem>()
}
}
}
deferreds.awaitAll().forEach { orders ->
allSells.addAll(orders)
}
}
val sortedSells = allSells.sortedByDescending { order ->
try {
SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
Locale.getDefault()
).parse(order.createdAt)
} catch (e: Exception) {
null
}
}
_sells.value = ViewState.Success(sortedSells)
Log.d(TAG, "All orders loaded successfully: ${sortedSells.size} items")
} else {
Log.d(TAG, "Calling repository.getSellList(status='$status')")
val startTime = System.currentTimeMillis()
@ -102,7 +153,10 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
order.orderItems?.let { items ->
Log.d(TAG, " Order items:")
items.forEachIndexed { itemIndex, item ->
Log.d(TAG, " Item ${itemIndex + 1}: ${item?.productName} (Qty: ${item?.quantity})")
Log.d(
TAG,
" Item ${itemIndex + 1}: ${item?.productName} (Qty: ${item?.quantity})"
)
}
}
}
@ -131,7 +185,7 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
// Keep the current loading state
}
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ Exception caught in getSellList")
Log.e(TAG, "Exception type: ${e.javaClass.simpleName}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -133,6 +133,7 @@
android:layout_marginTop="16dp"
android:text="Nomor Rekening / Nomor HP *"
android:fontFamily="@font/dmsans_semibold"
android:visibility="gone"
android:textSize="16sp" />
<EditText
@ -145,6 +146,7 @@
android:inputType="text"
android:minHeight="50dp"
android:textSize="14sp"
android:visibility="gone"
android:padding="12dp" />
<TextView
@ -152,6 +154,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Tanggal Pembayaran *"
android:visibility="gone"
android:fontFamily="@font/dmsans_semibold"
android:textSize="16sp" />
@ -164,6 +167,7 @@
android:drawableEnd="@drawable/ic_calendar"
android:drawablePadding="8dp"
android:hint="Pilih tanggal"
android:visibility="gone"
android:minHeight="50dp"
android:padding="12dp" />

View File

@ -21,6 +21,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header"/>
<ProgressBar
android:id="@+id/progressBarCart"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/bottomCheckoutLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header"/>
<TextView
android:id="@+id/tvWholesaleWarning"
android:layout_width="match_parent"
@ -110,12 +122,14 @@
android:src="@drawable/outline_shopping_cart_24" />
<TextView
android:id="@+id/emptyCart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Keranjang Anda kosong"
android:visibility="gone"
android:text="Keranjang anda kosong"
android:textColor="@android:color/black"
android:textSize="18sp" />
android:textSize="16sp" />
<TextView
android:layout_width="wrap_content"

View File

@ -6,7 +6,7 @@
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black_800"
android:background="@color/white"
android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.order.CheckoutActivity">
@ -75,7 +75,7 @@
android:id="@+id/tv_places_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rumah"
android:text="-"
android:textColor="#5A5A5A"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
@ -94,7 +94,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Jl. Pegangasan Timur"
android:text="-"
android:textSize="14sp"
android:layout_marginStart="32dp" />
@ -179,9 +179,11 @@
</LinearLayout>
<androidx.cardview.widget.CardView
android:id="@+id/card_shipment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone"
app:cardCornerRadius="8dp"
app:cardElevation="0dp"
app:cardBackgroundColor="#F5F5F5">

View File

@ -10,7 +10,7 @@
tools:context=".ui.profile.mystore.product.DetailStoreProductActivity">
<include
android:id="@+id/header"
android:id="@+id/headerStoreProduct"
layout="@layout/header" />
<ScrollView

View File

@ -3,9 +3,16 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:theme="@style/Theme.Ecommerce_serang"
tools:context=".ui.product.listproduct.ListProductActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/searchContainerList"
layout="@layout/view_search_back"
@ -13,8 +20,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/rvProductsList"/>
app:layout_constraintStart_toStartOf="parent"/>
<!-- <com.google.android.material.divider.MaterialDivider-->
<!-- android:id="@+id/divider_product"-->
@ -37,4 +43,8 @@
tools:listitem="@layout/item_product_grid"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,38 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_margin="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:paddingHorizontal="32dp"
android:paddingVertical="16dp"
tools:context=".ui.auth.LoginActivity">
<!-- Title -->
<TextView
android:layout_width="match_parent"
android:id="@+id/tv_login_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login"
android:textAlignment="center"
android:textSize="24sp"
android:textStyle="bold"
android:textAlignment="center"
android:layout_marginBottom="24dp"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/tv_email_label"
android:layout_marginBottom="48dp"
android:paddingBottom="24dp"/>
<!-- Email label -->
<TextView
android:layout_width="match_parent"
android:id="@+id/tv_email_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:text="@string/login_email"
android:textSize="18sp"
android:text="@string/login_email"/>
android:layout_marginVertical="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_login_title"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Email input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:id="@+id/til_login_email"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_email_label"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_login_email"
@ -42,20 +56,30 @@
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Password label-->
<TextView
android:layout_width="match_parent"
android:id="@+id/tv_password_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/dmsans_medium"
android:text="@string/password"
android:textSize="18sp"
android:text="@string/password"/>
android:layout_marginVertical="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_login_email"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Password input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:id="@+id/til_login_password"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:passwordToggleEnabled="true">
app:passwordToggleEnabled="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_password_label"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_login_password"
@ -65,28 +89,43 @@
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<!-- “Forgot password” link -->
<TextView
android:id="@+id/tv_forgetPassword"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/forget_password"
android:textColor="@android:color/holo_red_light"
android:textAlignment="textEnd"
android:layout_marginBottom="16dp"/>
android:textColor="@android:color/holo_red_light"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_login_password" />
<!-- Login button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/login"
app:cornerRadius="8dp"/>
app:cornerRadius="8dp"
android:layout_marginVertical="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_forgetPassword" />
<!-- “Dont have an account?” row (kept as LinearLayout) -->
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/ll_signup_row"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
android:orientation="horizontal"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_login">
<TextView
android:layout_width="wrap_content"
@ -102,4 +141,4 @@
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,7 +10,7 @@
tools:context=".ui.profile.mystore.MyStoreActivity">
<include
android:id="@+id/header"
android:id="@+id/headerMyStore"
layout="@layout/header" />
<ScrollView
@ -422,6 +422,7 @@
android:background="@color/black_50"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:visibility="gone"
android:id="@+id/layout_help"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -10,7 +10,7 @@
android:orientation="vertical">
<include
android:id="@+id/header"
android:id="@+id/headerListProduct"
layout="@layout/header" />
<!-- Search Bar -->

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:backgroundTint="@color/white"
tools:context=".ui.profile.mystore.review.ReviewActivity">
<include
android:id="@+id/header"
layout="@layout/header" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@drawable/baseline_star_24" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp">
<TextView
android:id="@+id/tv_review_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5.0"
style="@style/headline_small"
android:fontFamily="@font/dmsans_extrabold"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/5.0"
style="@style/body_medium"
android:textColor="@color/black_300"
app:layout_constraintStart_toEndOf="@id/tv_review_score"
app:layout_constraintBottom_toBottomOf="@id/tv_review_score"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/tv_total_review"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="318 rating"
style="@style/body_medium"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="•"
style="@style/body_medium"/>
<TextView
android:id="@+id/tv_total_review_with_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="108 ulasan"
style="@style/body_medium"/>
</LinearLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container_reviews"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -38,5 +38,6 @@
android:layout_marginBottom="8dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/linear_shipment"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.StoreOnReviewActivity"
android:orientation="vertical"
android:fitsSystemWindows="true">
<include
android:id="@+id/header"
layout="@layout/header" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/ic_under_review"
app:tint="@color/blue_500"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pengajuan toko Anda masih dalam tahap verifikasi oleh Admin"
style="@style/body_large"
android:fontFamily="@font/dmsans_extrabold"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>

View File

@ -1,14 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.product.ChangePriceBottomSheetFragment">
tools:context=".ui.profile.mystore.product.ChangePriceBottomSheetFragment"
android:orientation="vertical">
<!-- TODO: Update blank fragment layout -->
<TextView
<include
android:id="@+id/header"
layout="@layout/header" />
<!-- Input Harga dengan Prefix "Rp" -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="16dp">
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rp"
style="@style/label_medium_prominent"
android:textColor="@color/black_300"
android:padding="8dp" />
<EditText
android:id="@+id/edt_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:hint="Isi harga produk di sini"
android:inputType="number"
android:padding="8dp"
style="@style/body_small" />
</LinearLayout>
<Button
android:id="@+id/btn_save"
android:text="Simpan"
style="@style/button.large.active.long"
android:enabled="true"
android:layout_marginVertical="16dp"
android:layout_marginHorizontal="16dp"/>
</LinearLayout>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.product.ChangeStockBottomSheetFragment"
android:orientation="vertical">
<include
android:id="@+id/header"
layout="@layout/header" />
<!-- Input Harga dengan Prefix "Rp" -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_text_field"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="16dp"
android:gravity="center">
<ImageView
android:id="@+id/btn_minus"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="5dp"
android:src="@drawable/ic_minus"
android:clickable="true"
android:focusable="true"/>
<EditText
android:id="@+id/edt_stock"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:hint="Isi stok produk di sini"
android:inputType="number"
android:padding="8dp"
style="@style/body_small" />
<ImageView
android:id="@+id/btn_plus"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="5dp"
android:src="@drawable/ic_add"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
<Button
android:id="@+id/btn_save"
android:text="Simpan"
style="@style/button.large.active.long"
android:enabled="true"
android:layout_marginVertical="16dp"
android:layout_marginHorizontal="16dp"/>
</LinearLayout>

View File

@ -33,4 +33,23 @@
tools:listitem="@layout/item_chat"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<ProgressBar
android:id="@+id/progressBarChat"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="8dp"
android:visibility="gone"/>
<TextView
android:id="@+id/tv_empty_chat"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:gravity="center"
android:visibility="gone"
android:text="Pesan anda kosong"
android:textColor="@android:color/black"
android:textSize="16sp" />
</LinearLayout>

View File

@ -75,7 +75,7 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Didn't receive the code? " />
android:text="Belum menerima kode? " />
<TextView
android:id="@+id/tv_resend_otp"

View File

@ -5,6 +5,7 @@
android:layout_height="match_parent">
<ScrollView
android:id="@+id/sv_address_register"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/btn_register"
@ -149,7 +150,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kecamatan / Desa"
android:text="Kecamatan"
android:textColor="@android:color/black"
android:textSize="14sp" />
@ -157,18 +158,61 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Isi Kecamatan / Desa"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
android:hint="Pilih Kecamatan"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_kecamatan"
<AutoCompleteTextView
android:id="@+id/autoCompleteKecamatan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp"
android:inputType="textCapWords" />
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progress_bar_kecamatan"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- DESA / Kelurahan -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Kelurahan / Desa"
android:textColor="@android:color/black"
android:textSize="14sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Pilih Kelurahan / Desa"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteDesa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false"
android:clickable="true"
android:padding="12dp"
android:textSize="14sp" />
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/progress_bar_desa"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- Kode Pos -->
<TextView
android:layout_width="wrap_content"
@ -196,7 +240,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@drawable/bg_button_outline"
android:text="Previous"
android:text="Kembali"
android:textAllCaps="false"
android:textColor="@color/blue1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
@ -214,7 +258,8 @@
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/sv_address_register"/>
<ProgressBar
android:id="@+id/progress_bar"

View File

@ -1,13 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/reviews"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.review.ReviewFragment">
<TextView
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout_review"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello" />
android:layout_height="wrap_content"
app:tabMode="scrollable"
app:tabTextAppearance="@style/label_medium_prominent"
app:tabSelectedTextAppearance="@style/label_medium_prominent"
app:tabIndicatorColor="@color/blue_500"
app:tabSelectedTextColor="@color/blue_500"
app:tabTextColor="@color/black_300"
app:tabBackground="@color/white"
app:tabPadding="13dp"
app:layout_constraintTop_toTopOf="parent"/>
</FrameLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager_review"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_layout_review"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.profile.mystore.review.ReviewListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_review"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_review" />
<TextView
android:id="@+id/tv_empty_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tidak ada penilaian"
style="@style/body_large"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,7 +9,7 @@
tools:context=".ui.profile.mystore.sells.SellsFragment">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayoutSells"
android:id="@+id/tab_layout_sells"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMode="scrollable"
@ -23,10 +23,10 @@
app:layout_constraintTop_toTopOf="parent"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPagerSells"
android:id="@+id/view_pager_sells"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayoutSells" />
app:layout_constraintTop_toBottomOf="@+id/tab_layout_sells" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -7,7 +7,7 @@
tools:context=".ui.profile.mystore.sells.SellsListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSells"
android:id="@+id/rv_sells"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
@ -16,11 +16,11 @@
tools:listitem="@layout/item_sells" />
<TextView
android:id="@+id/tvEmptyState"
android:id="@+id/tv_empty_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TIdak ada penjualan"
android:textSize="16sp"
android:text="Tidak ada penjualan"
style="@style/body_large"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -28,7 +28,7 @@
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"

View File

@ -11,14 +11,15 @@
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
android:background="@null">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp">
app:contentInsetStartWithNavigation="0dp"
android:background="@null">
<ImageView
android:id="@+id/header_left_icon"

View File

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
@ -80,7 +82,9 @@
android:layout_height="24dp"
android:src="@drawable/ic_more_vertical"
android:contentDescription="Menu"
android:layout_marginStart="8dp" />
android:layout_marginStart="8dp"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:backgroundTint="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/iv_product"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/placeholder_image"
android:scaleType="centerCrop"
android:contentDescription="Review Product Image"
app:shapeAppearanceOverlay="@style/store_product_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/tv_product_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="16dp"
android:text="Jaket Pink Fuschia"
style="@style/body_medium"/>
<ImageView
android:id="@+id/iv_menu"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_more_vertical"
android:contentDescription="Menu"
android:layout_marginStart="8dp"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="8dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/baseline_star_24" />
<TextView
android:id="@+id/tv_review_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5.0"
style="@style/body_small" />
<TextView
android:id="@+id/tv_review_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="30-12-2025"
style="@style/body_small"
android:textColor="@color/black_300"/>
</LinearLayout>
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gracia"
android:layout_marginTop="8dp"
style="@style/label_medium_prominent"/>
<TextView
android:id="@+id/tv_review_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Suka banget! Real pict dan pengirimannya juga cepat! Next bakal beli di sini sih, thank you!"
android:layout_marginTop="8dp"
style="@style/label_small"/>
</LinearLayout>
<!-- Divider -->
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/black_50"/>
</LinearLayout>

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