3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
*.iml
|
*.iml
|
||||||
|
*.log
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/caches
|
/.idea/caches
|
||||||
@ -12,4 +13,4 @@
|
|||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
/app/google-services.json
|
||||||
|
68
README.md
Normal 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 buyer–seller chat
|
||||||
|
- Store registration and product management
|
||||||
|
- Store Balance as active status
|
||||||
|
- Top up store balance
|
||||||
|
- Write rating and feedback for purchased products
|
||||||
|
- Push notifications for user activity
|
||||||
|
|
||||||
|
The app communicates with a custom backend server via REST API and WebSocket.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- MVVM architecture
|
||||||
|
- Retrofit for API communication
|
||||||
|
- Hilt for dependency injection
|
||||||
|
- Socket.IO client for real-time chat
|
||||||
|
- Firebase Cloud Messaging (FCM) for notifications
|
||||||
|
- Coroutines for async operations
|
||||||
|
- Glide for image loading
|
||||||
|
- ViewBinding for UI access
|
||||||
|
- LiveData and StateFlow for reactive UI
|
||||||
|
- RajaOngkir API integration for shipping cost
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
- api/retrofit/
|
||||||
|
- data/
|
||||||
|
- di/
|
||||||
|
- ui/
|
||||||
|
- auth/
|
||||||
|
- home/
|
||||||
|
- cart/
|
||||||
|
- order/
|
||||||
|
- history/
|
||||||
|
- review/
|
||||||
|
- chat/
|
||||||
|
- profile/
|
||||||
|
- store/
|
||||||
|
- addProduct/
|
||||||
|
- sells/
|
||||||
|
- balance/
|
||||||
|
- review/
|
||||||
|
- product/
|
||||||
|
- notif/
|
||||||
|
- utils/
|
||||||
|
- google-services.json
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
1. Clone this project and open it in Android Studio
|
||||||
|
2. Add your `google-services.json` for Firebase (FCM)
|
||||||
|
3. Update the API base URL in the Retrofit client
|
||||||
|
5. Settings BASE_URL in your local.properties
|
||||||
|
4. Build and run on an emulator or physical device
|
||||||
|
|
||||||
|
|
3
app/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/build
|
/build
|
||||||
|
google-services.json
|
@ -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"
|
|
||||||
}
|
|
@ -82,11 +82,11 @@
|
|||||||
<!-- android:name="androidx.startup.InitializationProvider" -->
|
<!-- android:name="androidx.startup.InitializationProvider" -->
|
||||||
<!-- android:authorities="${applicationId}.androidx-startup" -->
|
<!-- android:authorities="${applicationId}.androidx-startup" -->
|
||||||
<!-- tools:node="remove" /> -->
|
<!-- tools:node="remove" /> -->
|
||||||
<service
|
<!-- <service-->
|
||||||
android:name=".ui.notif.SimpleWebSocketService"
|
<!-- android:name=".ui.notif.SimpleWebSocketService"-->
|
||||||
android:enabled="true"
|
<!-- android:enabled="true"-->
|
||||||
android:exported="false"
|
<!-- android:exported="false"-->
|
||||||
android:foregroundServiceType="dataSync" />
|
<!-- android:foregroundServiceType="dataSync" />-->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.profile.mystore.chat.ChatStoreActivity"
|
android:name=".ui.profile.mystore.chat.ChatStoreActivity"
|
||||||
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 27 KiB |
@ -2,12 +2,15 @@ package com.alya.ecommerce_serang.data.api.dto
|
|||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class CreateAddressRequest (
|
data class CreateAddressRequest(
|
||||||
|
@SerializedName("userId")
|
||||||
|
val userId: Int,
|
||||||
|
|
||||||
@SerializedName("latitude")
|
@SerializedName("latitude")
|
||||||
val lat: Double? = null,
|
val lat: Double,
|
||||||
|
|
||||||
@SerializedName("longitude")
|
@SerializedName("longitude")
|
||||||
val long: Double? = null,
|
val long: Double,
|
||||||
|
|
||||||
@SerializedName("street")
|
@SerializedName("street")
|
||||||
val street: String,
|
val street: String,
|
||||||
@ -16,26 +19,26 @@ data class CreateAddressRequest (
|
|||||||
val subDistrict: String,
|
val subDistrict: String,
|
||||||
|
|
||||||
@SerializedName("city_id")
|
@SerializedName("city_id")
|
||||||
val cityId: Int,
|
val cityId: String,
|
||||||
|
|
||||||
@SerializedName("province_id")
|
@SerializedName("province_id")
|
||||||
val provId: Int,
|
val provId: Int,
|
||||||
|
|
||||||
@SerializedName("postal_code")
|
@SerializedName("postal_code")
|
||||||
val postCode: String? = null,
|
val postCode: String,
|
||||||
|
|
||||||
|
@SerializedName("village_id")
|
||||||
|
val idVillage: String?, // nullable for now
|
||||||
|
|
||||||
@SerializedName("detail")
|
@SerializedName("detail")
|
||||||
val detailAddress: String? = null,
|
val detailAddress: String,
|
||||||
|
|
||||||
@SerializedName("user_id")
|
@SerializedName("is_store_location")
|
||||||
val userId: Int,
|
val isStoreLocation: Boolean,
|
||||||
|
|
||||||
@SerializedName("recipient")
|
@SerializedName("recipient")
|
||||||
val recipient: String,
|
val recipient: String,
|
||||||
|
|
||||||
@SerializedName("phone")
|
@SerializedName("phone")
|
||||||
val phone: String,
|
val phone: String
|
||||||
|
|
||||||
@SerializedName("is_store_location")
|
|
||||||
val isStoreLocation: Boolean
|
|
||||||
|
|
||||||
)
|
)
|
@ -98,5 +98,5 @@ data class Store(
|
|||||||
val storeDescription: String,
|
val storeDescription: String,
|
||||||
|
|
||||||
@field:SerializedName("city_id")
|
@field:SerializedName("city_id")
|
||||||
val cityId: Int
|
val cityId: String
|
||||||
)
|
)
|
||||||
|
@ -12,10 +12,8 @@ data class ListProvinceResponse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class ProvincesItem(
|
data class ProvincesItem(
|
||||||
|
|
||||||
@field:SerializedName("province")
|
|
||||||
val province: String,
|
|
||||||
|
|
||||||
@field:SerializedName("province_id")
|
@field:SerializedName("province_id")
|
||||||
val provinceId: String
|
val provinceId: String,
|
||||||
|
@field:SerializedName("province")
|
||||||
|
val province: String
|
||||||
)
|
)
|
||||||
|
@ -56,7 +56,7 @@ data class Orders(
|
|||||||
val orderItems: List<OrderListItemsItem>,
|
val orderItems: List<OrderListItemsItem>,
|
||||||
|
|
||||||
@field:SerializedName("auto_completed_at")
|
@field:SerializedName("auto_completed_at")
|
||||||
val autoCompletedAt: String? = null,
|
val autoCompletedAt: String,
|
||||||
|
|
||||||
@field:SerializedName("is_store_location")
|
@field:SerializedName("is_store_location")
|
||||||
val isStoreLocation: Boolean? = null,
|
val isStoreLocation: Boolean? = null,
|
||||||
|
@ -15,7 +15,7 @@ data class OrderListResponse(
|
|||||||
data class OrderItemsItem(
|
data class OrderItemsItem(
|
||||||
|
|
||||||
@field:SerializedName("review_id")
|
@field:SerializedName("review_id")
|
||||||
val reviewId: Int? = null,
|
val reviewId: Int,
|
||||||
|
|
||||||
@field:SerializedName("quantity")
|
@field:SerializedName("quantity")
|
||||||
val quantity: Int,
|
val quantity: Int,
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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
|
||||||
|
)
|
@ -4,15 +4,18 @@ import com.google.gson.annotations.SerializedName
|
|||||||
|
|
||||||
data class AddressResponse(
|
data class AddressResponse(
|
||||||
|
|
||||||
@field:SerializedName("addresses")
|
@field:SerializedName("addresses")
|
||||||
val addresses: List<AddressesItem>,
|
val addresses: List<AddressesItem>,
|
||||||
|
|
||||||
@field:SerializedName("message")
|
@field:SerializedName("message")
|
||||||
val message: String
|
val message: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class AddressesItem(
|
data class AddressesItem(
|
||||||
|
|
||||||
|
@field:SerializedName("village_id")
|
||||||
|
val villageId: String,
|
||||||
|
|
||||||
@field:SerializedName("is_store_location")
|
@field:SerializedName("is_store_location")
|
||||||
val isStoreLocation: Boolean,
|
val isStoreLocation: Boolean,
|
||||||
|
|
||||||
@ -23,7 +26,7 @@ data class AddressesItem(
|
|||||||
val userId: Int,
|
val userId: Int,
|
||||||
|
|
||||||
@field:SerializedName("province_id")
|
@field:SerializedName("province_id")
|
||||||
val provinceId: Int,
|
val provinceId: String,
|
||||||
|
|
||||||
@field:SerializedName("phone")
|
@field:SerializedName("phone")
|
||||||
val phone: String,
|
val phone: String,
|
||||||
@ -50,5 +53,5 @@ data class AddressesItem(
|
|||||||
val longitude: String,
|
val longitude: String,
|
||||||
|
|
||||||
@field:SerializedName("city_id")
|
@field:SerializedName("city_id")
|
||||||
val cityId: Int
|
val cityId: String
|
||||||
)
|
)
|
||||||
|
@ -15,10 +15,14 @@ class ApiConfig {
|
|||||||
|
|
||||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
|
//httplogginginterceptor ntuk debug dan monitoring request/response
|
||||||
}
|
}
|
||||||
|
|
||||||
val authInterceptor = AuthInterceptor(tokenManager)
|
val authInterceptor = AuthInterceptor(tokenManager)
|
||||||
|
// utk tambak token auth otomatis pada header
|
||||||
|
|
||||||
|
// Konfigurasi OkHttpClient
|
||||||
|
//Low-level HTTP client yang melakukan actual network request
|
||||||
val client = OkHttpClient.Builder()
|
val client = OkHttpClient.Builder()
|
||||||
.addInterceptor(loggingInterceptor)
|
.addInterceptor(loggingInterceptor)
|
||||||
.addInterceptor(authInterceptor)
|
.addInterceptor(authInterceptor)
|
||||||
@ -27,13 +31,17 @@ class ApiConfig {
|
|||||||
.writeTimeout(300, TimeUnit.SECONDS) // 5 minutes
|
.writeTimeout(300, TimeUnit.SECONDS) // 5 minutes
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
// Konfigurasi Retrofit
|
||||||
val retrofit = Retrofit.Builder()
|
val retrofit = Retrofit.Builder()
|
||||||
|
//almat domain backend
|
||||||
.baseUrl(BuildConfig.BASE_URL)
|
.baseUrl(BuildConfig.BASE_URL)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
//gson convertes: mengkonversi JSON ke object Kotlin dan sebaliknya
|
||||||
.client(client)
|
.client(client)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return retrofit.create(ApiService::class.java)
|
return retrofit.create(ApiService::class.java)
|
||||||
|
// retrofit : menyederhanakan HTTP Request dgn mengubah interface Kotlin di ApiService menjadi HTTP calls secara otomatis
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUnauthenticatedApiService(): ApiService {
|
fun getUnauthenticatedApiService(): ApiService {
|
||||||
|
@ -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.ListProvinceResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderDetailResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.product.AllProductResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.product.CategoryResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.product.DetailStoreProductResponse
|
||||||
@ -68,14 +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.order.CompletedOrderResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
|
import com.alya.ecommerce_serang.data.api.response.product.CreateSearchResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
|
import com.alya.ecommerce_serang.data.api.response.product.SearchHistoryResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
|
import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
|
import com.alya.ecommerce_serang.data.api.response.store.product.CreateProductResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
|
import com.alya.ecommerce_serang.data.api.response.store.product.DeleteProductResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
|
import com.alya.ecommerce_serang.data.api.response.store.product.UpdateProductResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
|
import com.alya.ecommerce_serang.data.api.response.store.product.ViewStoreProductsResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.GenericResponse
|
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
|
import com.alya.ecommerce_serang.data.api.response.store.review.ProductReviewResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.store.sells.PaymentConfirmationResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
|
import com.alya.ecommerce_serang.data.api.response.store.topup.BalanceTopUpResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
|
import com.alya.ecommerce_serang.data.api.response.store.topup.TopUpResponse
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
@ -512,4 +514,14 @@ interface ApiService {
|
|||||||
@GET("store/reviews")
|
@GET("store/reviews")
|
||||||
suspend fun getStoreProductReview(
|
suspend fun getStoreProductReview(
|
||||||
): Response<ProductReviewResponse>
|
): 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>
|
||||||
}
|
}
|
@ -1,10 +1,12 @@
|
|||||||
package com.alya.ecommerce_serang.data.repository
|
package com.alya.ecommerce_serang.data.repository
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.alya.ecommerce_serang.data.api.dto.ProductsItem
|
||||||
import com.alya.ecommerce_serang.data.api.dto.Store
|
import com.alya.ecommerce_serang.data.api.dto.Store
|
||||||
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
|
import com.alya.ecommerce_serang.data.api.response.auth.ListStoreTypeResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.store.sells.OrderListResponse
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@ -71,4 +73,104 @@ class MyStoreRepository(private val apiService: ApiService) {
|
|||||||
street, subdistrict, detail, postalCode, latitude, longitude, userPhone, storeType, storeimg
|
street, subdistrict, detail, postalCode, latitude, longitude, userPhone, storeType, storeimg
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getSellList(status: String): Result<OrderListResponse> {
|
||||||
|
return try {
|
||||||
|
Log.d("SellsRepository", "Add Evidence : $status")
|
||||||
|
val response = apiService.getSellList(status)
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val allListSell = response.body()
|
||||||
|
if (allListSell != null) {
|
||||||
|
Log.d("SellsRepository", "Add Evidence successfully: ${allListSell.message}")
|
||||||
|
Result.Success(allListSell)
|
||||||
|
} else {
|
||||||
|
Log.e("SellsRepository", "Response body was null")
|
||||||
|
Result.Error(Exception("Empty response from server"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||||
|
Log.e("SellsRepository", "Error Add Evidence : $errorBody")
|
||||||
|
Result.Error(Exception(errorBody))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SellsRepository", "Exception Add Evidence ", e)
|
||||||
|
Result.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBalance(): Result<com.alya.ecommerce_serang.data.api.response.store.StoreResponse> {
|
||||||
|
return try {
|
||||||
|
val response = apiService.getMyStoreData()
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val body = response.body()
|
||||||
|
?: return Result.Error(IllegalStateException("Response body is null"))
|
||||||
|
|
||||||
|
// Validate the balance field
|
||||||
|
val balanceRaw = body.store.balance
|
||||||
|
balanceRaw.toDoubleOrNull()
|
||||||
|
?: return Result.Error(NumberFormatException("Invalid balance format: $balanceRaw"))
|
||||||
|
|
||||||
|
Result.Success(body)
|
||||||
|
} else {
|
||||||
|
Result.Error(
|
||||||
|
Exception("Failed to load balance: ${response.code()} ${response.message()}")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MyStoreRepository", "Error fetching balance", e)
|
||||||
|
Result.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchMyStoreProducts(): List<ProductsItem> {
|
||||||
|
return try {
|
||||||
|
val response = apiService.getStoreProduct()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body()?.products?.filterNotNull() ?: emptyList()
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to fetch store products: ${response.message()}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ProductRepository", "Error fetching store products", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun fetchBalance() {
|
||||||
|
// showLoading(true)
|
||||||
|
// lifecycleScope.launch {
|
||||||
|
// try {
|
||||||
|
// val response = ApiConfig.getApiService(sessionManager).getMyStoreData()
|
||||||
|
// if (response.isSuccessful && response.body() != null) {
|
||||||
|
// val storeData = response.body()!!
|
||||||
|
// val balance = storeData.store.balance
|
||||||
|
//
|
||||||
|
// // Format the balance
|
||||||
|
// try {
|
||||||
|
// val balanceValue = balance.toDouble()
|
||||||
|
// binding.tvBalance.text = String.format("Rp%,.0f", balanceValue)
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// binding.tvBalance.text = "Rp$balance"
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Toast.makeText(
|
||||||
|
// this@BalanceActivity,
|
||||||
|
// "Gagal memuat data saldo: ${response.message()}",
|
||||||
|
// Toast.LENGTH_SHORT
|
||||||
|
// ).show()
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e(TAG, "Error fetching balance", e)
|
||||||
|
// Toast.makeText(
|
||||||
|
// this@BalanceActivity,
|
||||||
|
// "Error: ${e.message}",
|
||||||
|
// Toast.LENGTH_SHORT
|
||||||
|
// ).show()
|
||||||
|
// } finally {
|
||||||
|
// showLoading(false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
@ -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.auth.VerifRegisterResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.ListCityResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.ListProvinceResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictResponse
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import com.alya.ecommerce_serang.utils.FileUtils
|
import com.alya.ecommerce_serang.utils.FileUtils
|
||||||
@ -68,6 +70,16 @@ class UserRepository(private val apiService: ApiService) {
|
|||||||
return if (response.isSuccessful) response.body() else null
|
return if (response.isSuccessful) response.body() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getListSubdistrict(cityId : String): SubdistrictResponse? {
|
||||||
|
val response = apiService.getSubdistrict(cityId)
|
||||||
|
return if (response.isSuccessful) response.body() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getListVillages(subId: String): VillagesResponse? {
|
||||||
|
val response = apiService.getVillages(subId)
|
||||||
|
return if (response.isSuccessful) response.body() else null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun registerUser(request: RegisterRequest): RegisterResponse {
|
suspend fun registerUser(request: RegisterRequest): RegisterResponse {
|
||||||
val response = apiService.register(request) // API call
|
val response = apiService.register(request) // API call
|
||||||
|
|
||||||
@ -87,7 +99,7 @@ class UserRepository(private val apiService: ApiService) {
|
|||||||
longitude: String,
|
longitude: String,
|
||||||
street: String,
|
street: String,
|
||||||
subdistrict: String,
|
subdistrict: String,
|
||||||
cityId: Int,
|
cityId: String,
|
||||||
provinceId: Int,
|
provinceId: Int,
|
||||||
postalCode: Int,
|
postalCode: Int,
|
||||||
detail: String,
|
detail: String,
|
||||||
|
@ -8,9 +8,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import com.alya.ecommerce_serang.data.api.dto.FcmReq
|
import com.alya.ecommerce_serang.data.api.dto.FcmReq
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
@ -43,20 +41,18 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
// Apply insets to your root layout
|
// ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
// val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
// view.setPadding(
|
||||||
view.setPadding(
|
// systemBars.left,
|
||||||
systemBars.left,
|
// systemBars.top,
|
||||||
systemBars.top,
|
// systemBars.right,
|
||||||
systemBars.right,
|
// systemBars.bottom
|
||||||
systemBars.bottom
|
// )
|
||||||
)
|
// windowInsets
|
||||||
windowInsets
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
// onBackPressedDispatcher.addCallback(this) {
|
// onBackPressedDispatcher.addCallback(this) {
|
||||||
// // Handle the back button event
|
// // Handle the back button event
|
||||||
@ -105,6 +101,7 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
|
is com.alya.ecommerce_serang.data.repository.Result.Error -> {
|
||||||
|
Log.e("LoginActivity", "Login Failed: ${result.exception.message}")
|
||||||
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Login Failed: ${result.exception.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
is Result.Loading -> {
|
is Result.Loading -> {
|
||||||
|
@ -43,24 +43,6 @@ class RegisterActivity : AppCompatActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
sessionManager = SessionManager(this)
|
sessionManager = SessionManager(this)
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
|
|
||||||
enableEdgeToEdge()
|
|
||||||
|
|
||||||
// Apply insets to your root layout
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
|
||||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
view.setPadding(
|
|
||||||
systemBars.left,
|
|
||||||
systemBars.top,
|
|
||||||
systemBars.right,
|
|
||||||
systemBars.bottom
|
|
||||||
)
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'")
|
Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'")
|
||||||
Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'")
|
Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'")
|
||||||
|
|
||||||
@ -104,7 +86,7 @@ class RegisterActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to navigate to the next fragment
|
// navigate step register in fragment
|
||||||
fun navigateToStep(step: Int, userData: RegisterRequest?) {
|
fun navigateToStep(step: Int, userData: RegisterRequest?) {
|
||||||
val fragment = when (step) {
|
val fragment = when (step) {
|
||||||
1 -> RegisterStep1Fragment.newInstance()
|
1 -> RegisterStep1Fragment.newInstance()
|
||||||
|
@ -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() {
|
private fun validateAndProceed() {
|
||||||
|
@ -88,7 +88,7 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
|
|
||||||
// Update the email sent message
|
// Update the email sent message
|
||||||
userData?.let {
|
userData?.let {
|
||||||
binding.tvEmailSent.text = "We've sent a verification code to ${it.email}"
|
binding.tvEmailSent.text = "Kami telah mengirimkan kode OTP ke alamat email ${it.email}. Silahkan periksa email anda."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the resend cooldown timer
|
// Start the resend cooldown timer
|
||||||
@ -119,7 +119,7 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
Log.d(TAG, "verifyOtp called with OTP: $otp")
|
Log.d(TAG, "verifyOtp called with OTP: $otp")
|
||||||
|
|
||||||
if (otp.isEmpty()) {
|
if (otp.isEmpty()) {
|
||||||
Toast.makeText(requireContext(), "Please enter the verification code", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Masukkan kode OTP anda", Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,13 +153,13 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
}
|
}
|
||||||
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
|
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
Toast.makeText(requireContext(), "Verification code resent", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Kode OTP sudah dikirim", Toast.LENGTH_SHORT).show()
|
||||||
startResendCooldown()
|
startResendCooldown()
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
Log.e(TAG, "OTP request: Error - ${result.exception.message}")
|
Log.e(TAG, "OTP request: Error - ${result.exception.message}")
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
Toast.makeText(requireContext(), "Failed to resend code: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Gagal mengirim kode OTP", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "OTP request: Unknown state")
|
Log.d(TAG, "OTP request: Unknown state")
|
||||||
@ -180,7 +180,7 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
countDownTimer = object : CountDownTimer(30000, 1000) {
|
countDownTimer = object : CountDownTimer(30000, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
timeRemaining = (millisUntilFinished / 1000).toInt()
|
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) {
|
if (timeRemaining % 5 == 0) {
|
||||||
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
|
Log.d(TAG, "Cooldown remaining: $timeRemaining seconds")
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
|
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
Log.d(TAG, "Cooldown finished, enabling resend button")
|
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.isEnabled = true
|
||||||
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
|
binding.tvResendOtp.setTextColor(ContextCompat.getColor(requireContext(), R.color.blue1))
|
||||||
timeRemaining = 0
|
timeRemaining = 0
|
||||||
@ -222,7 +222,8 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
binding.btnVerify.isEnabled = true
|
binding.btnVerify.isEnabled = true
|
||||||
|
|
||||||
// Show error message
|
// Show error message
|
||||||
Toast.makeText(requireContext(), "Registration Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
Log.e(TAG, "Registration Failed: ${result.exception.message}")
|
||||||
|
Toast.makeText(requireContext(), "Gagal melakukan regsitrasi", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(TAG, "Registration: Unknown state")
|
Log.d(TAG, "Registration: Unknown state")
|
||||||
@ -251,15 +252,10 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
sessionManager.saveToken(accessToken)
|
sessionManager.saveToken(accessToken)
|
||||||
Log.d(TAG, "Token saved to SessionManager: $accessToken")
|
Log.d(TAG, "Token saved to SessionManager: $accessToken")
|
||||||
|
|
||||||
// Also save user ID if available in the login response
|
|
||||||
// result.data.?.let { userId ->
|
|
||||||
// sessionManager.saveUserId(userId)
|
|
||||||
// }
|
|
||||||
|
|
||||||
Log.d(TAG, "Login successful, token saved: $accessToken")
|
|
||||||
|
|
||||||
// Proceed to Step 3
|
// Proceed to Step 3
|
||||||
Log.d(TAG, "Proceeding to Step 3 after successful login")
|
Log.d(TAG, "Proceeding to Step 3 after successful login")
|
||||||
|
|
||||||
|
// call navigate register step from activity
|
||||||
(activity as? RegisterActivity)?.navigateToStep(3, null )
|
(activity as? RegisterActivity)?.navigateToStep(3, null )
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
@ -269,7 +265,7 @@ class RegisterStep2Fragment : Fragment() {
|
|||||||
|
|
||||||
// Show error message but continue to Step 3 anyway
|
// Show error message but continue to Step 3 anyway
|
||||||
Log.e(TAG, "Login failed but proceeding to Step 3", result.exception)
|
Log.e(TAG, "Login failed but proceeding to Step 3", result.exception)
|
||||||
Toast.makeText(requireContext(), "Note: Auto-login failed, but registration was successful", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Berhasil membuat akun, namun belum login", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
// Proceed to Step 3
|
// Proceed to Step 3
|
||||||
(activity as? RegisterActivity)?.navigateToStep(3, null)
|
(activity as? RegisterActivity)?.navigateToStep(3, null)
|
||||||
|
@ -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.auth.RegisterActivity
|
||||||
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
|
import com.alya.ecommerce_serang.ui.order.address.CityAdapter
|
||||||
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
|
import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
|
||||||
|
import com.alya.ecommerce_serang.ui.order.address.SubdsitrictAdapter
|
||||||
import com.alya.ecommerce_serang.ui.order.address.ViewState
|
import com.alya.ecommerce_serang.ui.order.address.ViewState
|
||||||
|
import com.alya.ecommerce_serang.ui.order.address.VillagesAdapter
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.RegisterViewModel
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.google.gson.Gson
|
||||||
|
|
||||||
class RegisterStep3Fragment : Fragment() {
|
class RegisterStep3Fragment : Fragment() {
|
||||||
private var _binding: FragmentRegisterStep3Binding? = null
|
private var _binding: FragmentRegisterStep3Binding? = null
|
||||||
@ -49,6 +52,8 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
// For province and city selection
|
// For province and city selection
|
||||||
private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) }
|
private val provinceAdapter by lazy { ProvinceAdapter(requireContext()) }
|
||||||
private val cityAdapter by lazy { CityAdapter(requireContext()) }
|
private val cityAdapter by lazy { CityAdapter(requireContext()) }
|
||||||
|
private val subdistrictAdapter by lazy { SubdsitrictAdapter(requireContext()) }
|
||||||
|
private val villagesAdapter by lazy { VillagesAdapter(requireContext()) }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "RegisterStep3Fragment"
|
private const val TAG = "RegisterStep3Fragment"
|
||||||
@ -114,7 +119,7 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
// Observe address submission state
|
// Observe address submission state
|
||||||
observeAddressSubmissionState()
|
observeAddressSubmissionState()
|
||||||
|
|
||||||
// Load provinces
|
// Load provinces from raja ongkir
|
||||||
Log.d(TAG, "Requesting provinces data")
|
Log.d(TAG, "Requesting provinces data")
|
||||||
registerViewModel.getProvinces()
|
registerViewModel.getProvinces()
|
||||||
setupProvinceObserver()
|
setupProvinceObserver()
|
||||||
@ -171,9 +176,10 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAutoComplete() {
|
private fun setupAutoComplete() {
|
||||||
// Same implementation as before
|
|
||||||
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
|
binding.autoCompleteProvinsi.setAdapter(provinceAdapter)
|
||||||
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
|
binding.autoCompleteKabupaten.setAdapter(cityAdapter)
|
||||||
|
binding.autoCompleteKecamatan.setAdapter(subdistrictAdapter)
|
||||||
|
binding.autoCompleteDesa.setAdapter(villagesAdapter)
|
||||||
|
|
||||||
binding.autoCompleteProvinsi.setOnClickListener {
|
binding.autoCompleteProvinsi.setOnClickListener {
|
||||||
binding.autoCompleteProvinsi.showDropDown()
|
binding.autoCompleteProvinsi.showDropDown()
|
||||||
@ -188,6 +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, _ ->
|
binding.autoCompleteProvinsi.setOnItemClickListener { _, _, position, _ ->
|
||||||
val provinceId = provinceAdapter.getProvinceId(position)
|
val provinceId = provinceAdapter.getProvinceId(position)
|
||||||
Log.d(TAG, "Province selected at position $position, ID: $provinceId")
|
Log.d(TAG, "Province selected at position $position, ID: $provinceId")
|
||||||
@ -206,13 +230,44 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
|
|
||||||
cityId?.let { id ->
|
cityId?.let { id ->
|
||||||
Log.d(TAG, "Selected city ID set to: $id")
|
Log.d(TAG, "Selected city ID set to: $id")
|
||||||
registerViewModel.selectedCityId = id
|
registerViewModel.updateSelectedCityId(id)
|
||||||
|
registerViewModel.getSubdistrict(id)
|
||||||
|
binding.autoCompleteKecamatan.text.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.autoCompleteKecamatan.setOnItemClickListener{ _, _, position, _ ->
|
||||||
|
val subdistrictId = subdistrictAdapter.getSubdistrictId(position)
|
||||||
|
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() {
|
private fun setupProvinceObserver() {
|
||||||
// Same implementation as before
|
// pake raja ongkir
|
||||||
registerViewModel.provincesState.observe(viewLifecycleOwner) { state ->
|
registerViewModel.provincesState.observe(viewLifecycleOwner) { state ->
|
||||||
when (state) {
|
when (state) {
|
||||||
is ViewState.Loading -> {
|
is ViewState.Loading -> {
|
||||||
@ -256,8 +311,46 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
registerViewModel.subdistrictState.observe(viewLifecycleOwner) { state ->
|
||||||
|
when (state) {
|
||||||
|
is ViewState.Loading -> {
|
||||||
|
binding.progressBarKecamatan.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
is ViewState.Success -> {
|
||||||
|
Log.d(TAG, "Subdistrict: Success - received ${state.data.size} kecamatan")
|
||||||
|
binding.progressBarKecamatan.visibility = View.GONE
|
||||||
|
subdistrictAdapter.updateData(state.data)
|
||||||
|
Log.d(TAG, "Updated subdistrict adapter with ${state.data.size} items")
|
||||||
|
}
|
||||||
|
is ViewState.Error -> {
|
||||||
|
Log.e(TAG, "Subdistrict: Error - ${state.message}")
|
||||||
|
binding.progressBarKecamatan.visibility = View.GONE
|
||||||
|
showError("Failed to load kecamatan: ${state.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerViewModel.villagesState.observe(viewLifecycleOwner) { state ->
|
||||||
|
when (state) {
|
||||||
|
is ViewState.Loading -> {
|
||||||
|
binding.progressBarDesa.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
is ViewState.Success -> {
|
||||||
|
Log.d(TAG, "Village: Success - received ${state.data.size} desa")
|
||||||
|
binding.progressBarDesa.visibility = View.GONE
|
||||||
|
villagesAdapter.updateData(state.data)
|
||||||
|
Log.d(TAG, "Updated village adapter with ${state.data.size} items")
|
||||||
|
}
|
||||||
|
is ViewState.Error -> {
|
||||||
|
Log.e(TAG, "Village: Error - ${state.message}")
|
||||||
|
binding.progressBarDesa.visibility = View.GONE
|
||||||
|
showError("Failed to load desa: ${state.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun submitAddress() {
|
private fun submitAddress() {
|
||||||
Log.d(TAG, "submitAddress called")
|
Log.d(TAG, "submitAddress called")
|
||||||
if (!validateAddressForm()) {
|
if (!validateAddressForm()) {
|
||||||
@ -276,13 +369,16 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
Log.d(TAG, "Using user ID: $userId")
|
Log.d(TAG, "Using user ID: $userId")
|
||||||
|
|
||||||
val street = binding.etDetailAlamat.text.toString().trim()
|
val street = binding.etDetailAlamat.text.toString().trim()
|
||||||
val subDistrict = binding.etKecamatan.text.toString().trim()
|
|
||||||
val postalCode = binding.etKodePos.text.toString().trim()
|
|
||||||
val recipient = binding.etNamaPenerima.text.toString().trim()
|
val recipient = binding.etNamaPenerima.text.toString().trim()
|
||||||
val phone = binding.etNomorHp.text.toString().trim()
|
val phone = binding.etNomorHp.text.toString().trim()
|
||||||
|
val postalCode = binding.etKodePos.text.toString().trim()
|
||||||
|
|
||||||
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
|
val provinceId = registerViewModel.selectedProvinceId?.toInt() ?: 0
|
||||||
val cityId = registerViewModel.selectedCityId?.toInt() ?: 0
|
val cityId = registerViewModel.selectedCityId.toString()
|
||||||
|
val subDistrict = registerViewModel.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 - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
|
||||||
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
|
Log.d(TAG, "Address data - Recipient: $recipient, Phone: $phone")
|
||||||
@ -291,21 +387,25 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
|
|
||||||
// Create address request
|
// Create address request
|
||||||
val addressRequest = CreateAddressRequest(
|
val addressRequest = CreateAddressRequest(
|
||||||
|
userId = user.id, // must match the type expected in the DB
|
||||||
lat = defaultLatitude,
|
lat = defaultLatitude,
|
||||||
long = defaultLongitude,
|
long = defaultLongitude,
|
||||||
street = street,
|
street = street,
|
||||||
subDistrict = subDistrict,
|
subDistrict = subDistrict,
|
||||||
cityId = cityId,
|
cityId = cityId, // ⚠️ Make sure this is Int
|
||||||
provId = provinceId,
|
provId = provinceId,
|
||||||
postCode = postalCode,
|
postCode = postalCode,
|
||||||
|
idVillage = villageId, // Or provide a real ID if needed
|
||||||
detailAddress = street,
|
detailAddress = street,
|
||||||
userId = userId,
|
isStoreLocation = false,
|
||||||
recipient = recipient,
|
recipient = recipient,
|
||||||
phone = phone,
|
phone = phone
|
||||||
isStoreLocation = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(TAG, "Address request created: $addressRequest")
|
Log.d(TAG, "Address request created: $addressRequest")
|
||||||
|
val gson = Gson()
|
||||||
|
val jsonString = gson.toJson(addressRequest)
|
||||||
|
Log.d(TAG, "Request JSON: $jsonString")
|
||||||
|
|
||||||
// Show loading
|
// Show loading
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
@ -318,13 +418,13 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
|
|
||||||
private fun validateAddressForm(): Boolean {
|
private fun validateAddressForm(): Boolean {
|
||||||
val street = binding.etDetailAlamat.text.toString().trim()
|
val street = binding.etDetailAlamat.text.toString().trim()
|
||||||
val subDistrict = binding.etKecamatan.text.toString().trim()
|
|
||||||
val postalCode = binding.etKodePos.text.toString().trim()
|
|
||||||
val recipient = binding.etNamaPenerima.text.toString().trim()
|
val recipient = binding.etNamaPenerima.text.toString().trim()
|
||||||
val phone = binding.etNomorHp.text.toString().trim()
|
val phone = binding.etNomorHp.text.toString().trim()
|
||||||
|
|
||||||
val provinceId = registerViewModel.selectedProvinceId
|
val provinceId = registerViewModel.selectedProvinceId
|
||||||
val cityId = registerViewModel.selectedCityId
|
val cityId = registerViewModel.selectedCityId
|
||||||
|
val subDistrict = registerViewModel.selectedSubdistrict.toString()
|
||||||
|
val postalCode = registerViewModel.selectedPostalCode
|
||||||
|
|
||||||
Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
|
Log.d(TAG, "Validating - Street: $street, SubDistrict: $subDistrict, PostalCode: $postalCode")
|
||||||
Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone")
|
Log.d(TAG, "Validating - Recipient: $recipient, Phone: $phone")
|
||||||
@ -409,8 +509,4 @@ class RegisterStep3Fragment : Fragment() {
|
|||||||
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
|
ViewCompat.setWindowInsetsAnimationCallback(binding.root, null)
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// // Data classes for province and city
|
|
||||||
// data class Province(val id: String, val name: String)
|
|
||||||
// data class City(val id: String, val name: String)
|
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package com.alya.ecommerce_serang.ui.cart
|
package com.alya.ecommerce_serang.ui.cart
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@ -145,7 +146,9 @@ class CartActivity : AppCompatActivity() {
|
|||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
viewModel.cartItems.observe(this) { cartItems ->
|
viewModel.cartItems.observe(this) { cartItems ->
|
||||||
if (cartItems.isNullOrEmpty()) {
|
if (cartItems.isNullOrEmpty()) {
|
||||||
|
binding.emptyCart.visibility = View.VISIBLE
|
||||||
showEmptyState(true)
|
showEmptyState(true)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
showEmptyState(false)
|
showEmptyState(false)
|
||||||
storeAdapter.submitList(cartItems)
|
storeAdapter.submitList(cartItems)
|
||||||
@ -153,7 +156,8 @@ class CartActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModel.isLoading.observe(this) { isLoading ->
|
viewModel.isLoading.observe(this) { isLoading ->
|
||||||
// Show/hide loading indicator if needed
|
binding.progressBarCart?.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||||
|
Log.d("CartActivity", "Loading state: $isLoading")
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.errorMessage.observe(this) { errorMessage ->
|
viewModel.errorMessage.observe(this) { errorMessage ->
|
||||||
|
@ -27,6 +27,7 @@ import androidx.core.view.WindowCompat
|
|||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
@ -63,6 +64,8 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
// For image attachment
|
// For image attachment
|
||||||
private var tempImageUri: Uri? = null
|
private var tempImageUri: Uri? = null
|
||||||
|
|
||||||
|
private var imageAttach = false
|
||||||
|
|
||||||
// Typing indicator handler
|
// Typing indicator handler
|
||||||
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
private val typingHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||||
private val stopTypingRunnable = Runnable {
|
private val stopTypingRunnable = Runnable {
|
||||||
@ -127,6 +130,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set up data toko
|
||||||
binding.tvStoreName.text = storeName
|
binding.tvStoreName.text = storeName
|
||||||
val fullImageUrl = when (val img = storeImg) {
|
val fullImageUrl = when (val img = storeImg) {
|
||||||
is String -> {
|
is String -> {
|
||||||
@ -140,7 +144,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(binding.imgProfile)
|
.into(binding.imgProfile)
|
||||||
|
|
||||||
// Set chat parameters to ViewModel
|
// Set chat parameter to send to ViewModel with product
|
||||||
viewModel.setChatParameters(
|
viewModel.setChatParameters(
|
||||||
storeId = storeId,
|
storeId = storeId,
|
||||||
productId = productId,
|
productId = productId,
|
||||||
@ -157,16 +161,17 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup UI components
|
// Setup UI components
|
||||||
|
// rv isi chat
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupWindowInsets()
|
setupWindowInsets()
|
||||||
setupListeners()
|
setupListeners()
|
||||||
setupTypingIndicator()
|
setupTypingIndicator()
|
||||||
|
// observe listener from viewmodel
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
|
|
||||||
// If opened from ChatListFragment with a valid chatRoomId
|
// If opened from ChatListFragment with a valid chatRoomId
|
||||||
if (chatRoomId > 0) {
|
if (chatRoomId > 0) {
|
||||||
// Directly set the chatRoomId and load chat history
|
viewModel.setChatRoomId(chatRoomId)
|
||||||
viewModel._chatRoomId.value = chatRoomId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +274,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Options button
|
// Options button
|
||||||
|
binding.btnOptions.visibility = View.GONE
|
||||||
binding.btnOptions.setOnClickListener {
|
binding.btnOptions.setOnClickListener {
|
||||||
showOptionsMenu()
|
showOptionsMenu()
|
||||||
}
|
}
|
||||||
@ -281,6 +287,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
// This will automatically handle product attachment if enabled
|
// This will automatically handle product attachment if enabled
|
||||||
viewModel.sendMessage(message)
|
viewModel.sendMessage(message)
|
||||||
binding.editTextMessage.text.clear()
|
binding.editTextMessage.text.clear()
|
||||||
|
binding.layoutAttachImage.visibility = View.GONE
|
||||||
|
|
||||||
// Instantly scroll to show new message
|
// Instantly scroll to show new message
|
||||||
binding.recyclerChat.postDelayed({
|
binding.recyclerChat.postDelayed({
|
||||||
@ -291,24 +298,33 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Attachment button
|
// Attachment button
|
||||||
binding.btnAttachment.setOnClickListener {
|
binding.btnAttachment.setOnClickListener {
|
||||||
|
this.currentFocus?.let { view ->
|
||||||
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
}
|
||||||
checkPermissionsAndShowImagePicker()
|
checkPermissionsAndShowImagePicker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnCloseChat.setOnClickListener{
|
||||||
|
binding.layoutAttachImage.visibility = View.GONE
|
||||||
|
imageAttach = false
|
||||||
|
viewModel.clearSelectedImage()
|
||||||
|
}
|
||||||
|
|
||||||
// Product card click to enable/disable product attachment
|
// Product card click to enable/disable product attachment
|
||||||
binding.productContainer.setOnClickListener {
|
binding.productContainer.setOnClickListener {
|
||||||
toggleProductAttachment()
|
toggleProductAttachment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun toggleProductAttachment() {
|
private fun toggleProductAttachment() {
|
||||||
val currentState = viewModel.state.value
|
val currentState = viewModel.state.value
|
||||||
if (currentState?.hasProductAttachment == true) {
|
if (currentState?.hasProductAttachment == true) {
|
||||||
// Disable product attachment
|
|
||||||
viewModel.disableProductAttachment()
|
viewModel.disableProductAttachment()
|
||||||
updateProductAttachmentUI(false)
|
updateProductAttachmentUI(false)
|
||||||
Toast.makeText(this, "Product attachment disabled", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Product attachment disabled", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
// Enable product attachment
|
|
||||||
viewModel.enableProductAttachment()
|
viewModel.enableProductAttachment()
|
||||||
updateProductAttachmentUI(true)
|
updateProductAttachmentUI(true)
|
||||||
Toast.makeText(this, "Product will be attached to your next message", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Product will be attached to your next message", Toast.LENGTH_SHORT).show()
|
||||||
@ -389,77 +405,76 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.state.observe(this, Observer { state ->
|
lifecycleScope.launchWhenStarted {
|
||||||
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
|
viewModel.state.collect() { state ->
|
||||||
|
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
|
||||||
|
|
||||||
// Update messages
|
// Update messages
|
||||||
val previousCount = chatAdapter.itemCount
|
val previousCount = chatAdapter.itemCount
|
||||||
|
|
||||||
val displayItems = viewModel.getDisplayItems()
|
val displayItems = viewModel.getDisplayItems()
|
||||||
|
|
||||||
chatAdapter.submitList(displayItems) {
|
chatAdapter.submitList(displayItems) {
|
||||||
Log.d(TAG, "Messages submitted to adapter")
|
Log.d(TAG, "Messages submitted to adapter")
|
||||||
// Only auto-scroll for new messages or initial load
|
// Only auto-scroll for new messages or initial load
|
||||||
if (previousCount == 0 || state.messages.size > previousCount) {
|
if (previousCount == 0 || state.messages.size > previousCount) {
|
||||||
scrollToBottomInstant()
|
scrollToBottomInstant()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update product info
|
|
||||||
if (!state.productName.isNullOrEmpty()) {
|
|
||||||
binding.tvProductName.text = state.productName
|
|
||||||
binding.tvProductPrice.text = state.productPrice
|
|
||||||
binding.ratingBar.rating = state.productRating
|
|
||||||
binding.tvRating.text = state.productRating.toString()
|
|
||||||
binding.tvSellerName.text = state.storeName
|
|
||||||
binding.tvStoreName.text = state.storeName
|
|
||||||
|
|
||||||
val fullImageUrl = when (val img = state.productImageUrl) {
|
|
||||||
is String -> {
|
|
||||||
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
|
||||||
}
|
}
|
||||||
else -> R.drawable.placeholder_image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.productImageUrl.isNullOrEmpty()) {
|
// layout attach product
|
||||||
Glide.with(this@ChatActivity)
|
if (!state.productName.isNullOrEmpty()) {
|
||||||
.load(fullImageUrl)
|
binding.tvProductName.text = state.productName
|
||||||
.centerCrop()
|
binding.tvProductPrice.text = state.productPrice
|
||||||
.placeholder(R.drawable.placeholder_image)
|
binding.ratingBar.rating = state.productRating
|
||||||
.error(R.drawable.placeholder_image)
|
binding.tvRating.text = state.productRating.toString()
|
||||||
.into(binding.imgProduct)
|
binding.tvSellerName.text = state.storeName
|
||||||
|
binding.tvStoreName.text = state.storeName
|
||||||
|
|
||||||
|
val fullImageUrl = when (val img = state.productImageUrl) {
|
||||||
|
is String -> {
|
||||||
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> R.drawable.placeholder_image
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.productImageUrl.isNullOrEmpty()) {
|
||||||
|
Glide.with(this@ChatActivity)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.centerCrop()
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.error(R.drawable.placeholder_image)
|
||||||
|
.into(binding.imgProduct)
|
||||||
|
}
|
||||||
|
updateProductCardUI(state.hasProductAttachment)
|
||||||
|
|
||||||
|
binding.productContainer.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.productContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
updateProductCardUI(state.hasProductAttachment)
|
|
||||||
|
|
||||||
binding.productContainer.visibility = View.GONE
|
updateInputHint(state)
|
||||||
} else {
|
|
||||||
binding.productContainer.visibility = View.GONE
|
// Update attachment hint
|
||||||
|
if (state.hasAttachment) {
|
||||||
|
binding.layoutAttachImage.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.editTextMessage.hint = getString(R.string.write_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
state.error?.let { error ->
|
||||||
|
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
updateInputHint(state)
|
|
||||||
|
|
||||||
// Update attachment hint
|
|
||||||
if (state.hasAttachment) {
|
|
||||||
binding.editTextMessage.hint = getString(R.string.image_attached)
|
|
||||||
} else {
|
|
||||||
binding.editTextMessage.hint = getString(R.string.write_message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show typing indicator
|
|
||||||
binding.tvTypingIndicator.visibility =
|
|
||||||
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
// Show error if any
|
|
||||||
state.error?.let { error ->
|
|
||||||
Toast.makeText(this@ChatActivity, error, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearError()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateInputHint(state: ChatUiState) {
|
private fun updateInputHint(state: ChatUiState) {
|
||||||
binding.editTextMessage.hint = when {
|
binding.editTextMessage.hint = when {
|
||||||
state.hasAttachment -> getString(R.string.image_attached)
|
state.hasAttachment -> getString(R.string.write_message)
|
||||||
state.hasProductAttachment -> "Type your message (product will be attached)"
|
state.hasProductAttachment -> "Type your message (product will be attached)"
|
||||||
else -> getString(R.string.write_message)
|
else -> getString(R.string.write_message)
|
||||||
}
|
}
|
||||||
@ -480,7 +495,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Opening: ${productInfo.productName}", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
// You can navigate to product detail here
|
// You can navigate to product detail here
|
||||||
navigateToProductDetail(productInfo.productId)
|
navigateToProductDetail(productInfo.productId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToProductDetail(productId: Int) {
|
private fun navigateToProductDetail(productId: Int) {
|
||||||
@ -504,6 +519,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
getString(R.string.cancel)
|
getString(R.string.cancel)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(getString(R.string.options))
|
.setTitle(getString(R.string.options))
|
||||||
.setItems(options) { dialog, which ->
|
.setItems(options) { dialog, which ->
|
||||||
@ -578,7 +594,21 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun handleSelectedImage(uri: Uri) {
|
private fun handleSelectedImage(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Processing selected image: $uri")
|
Log.d(TAG, "Processing selected image: ${uri.toString()}")
|
||||||
|
imageAttach = true
|
||||||
|
binding.layoutAttachImage.visibility = View.VISIBLE
|
||||||
|
val fullImageUrl = when (val img = uri.toString()) {
|
||||||
|
is String -> {
|
||||||
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
|
}
|
||||||
|
else -> R.drawable.placeholder_image
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.into(binding.ivAttach)
|
||||||
|
Log.d(TAG, "Display attach image: $uri")
|
||||||
|
|
||||||
// Always use the copy-to-cache approach for reliability
|
// Always use the copy-to-cache approach for reliability
|
||||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
@ -598,6 +628,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}")
|
Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}")
|
||||||
viewModel.setSelectedImageFile(outputFile)
|
viewModel.setSelectedImageFile(outputFile)
|
||||||
|
|
||||||
Toast.makeText(this, "Image selected", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Image selected", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to create image file")
|
Log.e(TAG, "Failed to create image file")
|
||||||
@ -681,25 +712,4 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//if implement typing status
|
|
||||||
// private fun handleConnectionState(state: ConnectionState) {
|
|
||||||
// when (state) {
|
|
||||||
// is ConnectionState.Connected -> {
|
|
||||||
// binding.tvConnectionStatus.visibility = View.GONE
|
|
||||||
// }
|
|
||||||
// is ConnectionState.Connecting -> {
|
|
||||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
|
||||||
// binding.tvConnectionStatus.text = getString(R.string.connecting)
|
|
||||||
// }
|
|
||||||
// is ConnectionState.Disconnected -> {
|
|
||||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
|
||||||
// binding.tvConnectionStatus.text = getString(R.string.disconnected_reconnecting)
|
|
||||||
// }
|
|
||||||
// is ConnectionState.Error -> {
|
|
||||||
// binding.tvConnectionStatus.visibility = View.VISIBLE
|
|
||||||
// binding.tvConnectionStatus.text = getString(R.string.connection_error, state.message)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -209,7 +209,7 @@ class ChatAdapter(
|
|||||||
binding.tvProductPrice.text = product.productPrice
|
binding.tvProductPrice.text = product.productPrice
|
||||||
|
|
||||||
// Load product image
|
// Load product image
|
||||||
val fullImageUrl = if (product.productImage.startsWith("/")) {
|
val fullImageUrl = if (product.productImage!!.startsWith("/")) {
|
||||||
BASE_URL + product.productImage.substring(1)
|
BASE_URL + product.productImage.substring(1)
|
||||||
} else {
|
} else {
|
||||||
product.productImage
|
product.productImage
|
||||||
@ -246,7 +246,7 @@ class ChatAdapter(
|
|||||||
binding.tvProductPrice.text = product.productPrice
|
binding.tvProductPrice.text = product.productPrice
|
||||||
|
|
||||||
// Load product image
|
// Load product image
|
||||||
val fullImageUrl = if (product.productImage.startsWith("/")) {
|
val fullImageUrl = if (product.productImage!!.startsWith("/")) {
|
||||||
BASE_URL + product.productImage.substring(1)
|
BASE_URL + product.productImage.substring(1)
|
||||||
} else {
|
} else {
|
||||||
product.productImage
|
product.productImage
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.alya.ecommerce_serang.ui.chat
|
package com.alya.ecommerce_serang.ui.chat
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -55,31 +56,44 @@ class ChatListFragment : Fragment() {
|
|||||||
viewModel.chatList.observe(viewLifecycleOwner) { result ->
|
viewModel.chatList.observe(viewLifecycleOwner) { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
val adapter = ChatListAdapter(result.data) { chatItem ->
|
val data = result.data
|
||||||
// Use the ChatActivity.createIntent factory method for proper navigation
|
|
||||||
ChatActivity.createIntent(
|
binding.tvEmptyChat.visibility = View.GONE
|
||||||
context = requireActivity(),
|
if (data.isNotEmpty()) {
|
||||||
storeId = chatItem.storeId,
|
val adapter = ChatListAdapter(data) { chatItem ->
|
||||||
productId = 0, // Default value since we don't have it in ChatListItem
|
ChatActivity.createIntent(
|
||||||
productName = null, // Null is acceptable as per ChatActivity
|
context = requireActivity(),
|
||||||
productPrice = "",
|
storeId = chatItem.storeId,
|
||||||
productImage = null,
|
productId = 0,
|
||||||
productRating = null,
|
productName = null,
|
||||||
storeName = chatItem.storeName,
|
productPrice = "",
|
||||||
chatRoomId = chatItem.chatRoomId,
|
productImage = null,
|
||||||
storeImage = chatItem.storeImage
|
productRating = null,
|
||||||
)
|
storeName = chatItem.storeName,
|
||||||
|
chatRoomId = chatItem.chatRoomId,
|
||||||
|
storeImage = chatItem.storeImage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.chatListRecyclerView.adapter = adapter
|
||||||
|
} else {
|
||||||
|
binding.tvEmptyChat.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
binding.chatListRecyclerView.adapter = adapter
|
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
|
binding.tvEmptyChat.visibility = View.VISIBLE
|
||||||
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), "Failed to load chats", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
Result.Loading -> {
|
Result.Loading -> {
|
||||||
|
binding.progressBarChat.visibility = View.VISIBLE
|
||||||
// Optional: show progress bar
|
// Optional: show progress bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//loading chat list
|
||||||
|
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||||
|
binding.progressBarChat?.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||||
|
Log.d(TAG, "Loading state: $isLoading")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -89,6 +103,6 @@ class ChatListFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
|
private var TAG = "ChatListFragment"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,6 +14,9 @@ import com.alya.ecommerce_serang.data.repository.Result
|
|||||||
import com.alya.ecommerce_serang.utils.Constants
|
import com.alya.ecommerce_serang.utils.Constants
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -23,6 +26,28 @@ import java.util.Locale
|
|||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatViewModel - Manages chat functionality for both buyers and store owners
|
||||||
|
*
|
||||||
|
* ARCHITECTURE OVERVIEW:
|
||||||
|
* - Handles real-time messaging via Socket.IO
|
||||||
|
* - Manages chat state using LiveData/MutableLiveData pattern
|
||||||
|
* - Supports multiple message types: TEXT, IMAGE, PRODUCT
|
||||||
|
* - Maintains separate flows for buyer and store owner chat
|
||||||
|
*
|
||||||
|
* KEY RESPONSIBILITIES:
|
||||||
|
* 1. Socket connection management and real-time message handling
|
||||||
|
* 2. Message sending/receiving with different attachment types
|
||||||
|
* 3. Chat history loading and message status updates
|
||||||
|
* 4. Product attachment functionality for commerce integration
|
||||||
|
* 5. User session management and authentication
|
||||||
|
*
|
||||||
|
* STATE MANAGEMENT PATTERN:
|
||||||
|
* - All UI state updates go through updateState() helper function
|
||||||
|
* - State updates are atomic and follow immutable pattern
|
||||||
|
* - Error states are cleared explicitly via clearError()
|
||||||
|
*/
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ChatViewModel @Inject constructor(
|
class ChatViewModel @Inject constructor(
|
||||||
private val chatRepository: ChatRepository,
|
private val chatRepository: ChatRepository,
|
||||||
@ -34,9 +59,12 @@ class ChatViewModel @Inject constructor(
|
|||||||
// Product attachment flag
|
// Product attachment flag
|
||||||
private var shouldAttachProduct = false
|
private var shouldAttachProduct = false
|
||||||
|
|
||||||
// UI state using LiveData
|
// use state for more seamless responsive
|
||||||
private val _state = MutableLiveData(ChatUiState())
|
private val _state = MutableStateFlow(ChatUiState())
|
||||||
val state: LiveData<ChatUiState> = _state
|
val state: StateFlow<ChatUiState> = _state
|
||||||
|
|
||||||
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
|
val isLoading: LiveData<Boolean> = _isLoading
|
||||||
|
|
||||||
val _chatRoomId = MutableLiveData<Int>(0)
|
val _chatRoomId = MutableLiveData<Int>(0)
|
||||||
val chatRoomId: LiveData<Int> = _chatRoomId
|
val chatRoomId: LiveData<Int> = _chatRoomId
|
||||||
@ -68,16 +96,21 @@ class ChatViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "ChatViewModel initialized")
|
Log.d(TAG, "ChatViewModel initialized")
|
||||||
|
socketService.connect() // 🛠 force connection
|
||||||
|
setupSocketListeners() // 🛠 always listen, even before user data
|
||||||
initializeUser()
|
initializeUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeUser() {
|
private fun initializeUser() {
|
||||||
|
_isLoading.value = true
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Log.d(TAG, "Initializing user session...")
|
Log.d(TAG, "Initializing user session...")
|
||||||
|
|
||||||
when (val result = chatRepository.fetchUserProfile()) {
|
when (val result = chatRepository.fetchUserProfile()) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
currentUserId = result.data?.userId
|
currentUserId = result.data?.userId
|
||||||
|
_isLoading.value = false
|
||||||
|
|
||||||
Log.d(TAG, "User session initialized - User ID: $currentUserId")
|
Log.d(TAG, "User session initialized - User ID: $currentUserId")
|
||||||
|
|
||||||
if (currentUserId == null || currentUserId == 0) {
|
if (currentUserId == null || currentUserId == 0) {
|
||||||
@ -85,14 +118,17 @@ class ChatViewModel @Inject constructor(
|
|||||||
updateState { it.copy(error = "User authentication error. Please login again.") }
|
updateState { it.copy(error = "User authentication error. Please login again.") }
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Setting up socket listeners...")
|
Log.d(TAG, "Setting up socket listeners...")
|
||||||
|
socketService.connect()
|
||||||
setupSocketListeners()
|
setupSocketListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
|
_isLoading.value = false
|
||||||
Log.e(TAG, "Failed to fetch user profile: ${result.exception.message}")
|
Log.e(TAG, "Failed to fetch user profile: ${result.exception.message}")
|
||||||
updateState { it.copy(error = "User authentication error. Please login again.") }
|
updateState { it.copy(error = "User authentication error. Please login again.") }
|
||||||
}
|
}
|
||||||
is Result.Loading -> {
|
is Result.Loading -> {
|
||||||
|
_isLoading.value = true
|
||||||
Log.d(TAG, "Loading user profile...")
|
Log.d(TAG, "Loading user profile...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -201,26 +237,116 @@ class ChatViewModel @Inject constructor(
|
|||||||
|
|
||||||
if (connectionState is ConnectionState.Connected) {
|
if (connectionState is ConnectionState.Connected) {
|
||||||
Log.d(TAG, "Socket connected, joining room...")
|
Log.d(TAG, "Socket connected, joining room...")
|
||||||
socketService.joinRoom()
|
val roomId = _chatRoomId.value
|
||||||
|
if (roomId != null && roomId > 0) {
|
||||||
|
socketService.joinRoom(roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// socketService.newMessages.collect { chatLine ->
|
||||||
|
// chatLine?.let {
|
||||||
|
// Log.d(TAG, "NEW message received in ViewModel: ${it.message}")
|
||||||
|
// val updatedMessages = _state.value.messages.toMutableList()
|
||||||
|
// updatedMessages.add(convertChatLineToUiMessage(it))
|
||||||
|
// updateState { it.copy(messages = updatedMessages) }
|
||||||
|
//
|
||||||
|
// if (it.senderId != currentUserId) {
|
||||||
|
// updateMessageStatus(it.id, Constants.STATUS_READ)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
socketService.newMessages.collect { chatLine ->
|
socketService.newMessages.collect { chatLine ->
|
||||||
chatLine?.let {
|
Log.d("ChatViewModel", "Collected new message from SocketIOService: ${chatLine.message}")
|
||||||
Log.d(TAG, "New message received via socket - ID: ${it.id}, SenderID: ${it.senderId}")
|
chatLine?.let { incomingChatLine ->
|
||||||
val currentMessages = _state.value?.messages ?: listOf()
|
// 1. First update: Add the message to the list (potentially without full product info)
|
||||||
val updatedMessages = currentMessages.toMutableList().apply {
|
_state.update { currentState ->
|
||||||
add(convertChatLineToUiMessage(it))
|
val existingMessageIndex =
|
||||||
|
currentState.messages.indexOfFirst { it.id == incomingChatLine.id }
|
||||||
|
val messagesAfterInitialUpdate = if (existingMessageIndex != -1) {
|
||||||
|
// If message exists (e.g., status update), just update it
|
||||||
|
val updatedList = currentState.messages.toMutableList()
|
||||||
|
updatedList[existingMessageIndex] = mapChatLineToUiMessage(
|
||||||
|
incomingChatLine,
|
||||||
|
updatedList[existingMessageIndex].productInfo
|
||||||
|
) // Preserve existing productInfo if any
|
||||||
|
updatedList
|
||||||
|
} else {
|
||||||
|
// New message, add it
|
||||||
|
(currentState.messages + mapChatLineToUiMessage(incomingChatLine)).distinctBy { msg -> msg.id }
|
||||||
|
}
|
||||||
|
// Sort after any update/addition
|
||||||
|
currentState.copy(messages = messagesAfterInitialUpdate.sortedBy { msg ->
|
||||||
|
SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).parse(msg.createdAt)?.time
|
||||||
|
})
|
||||||
}
|
}
|
||||||
updateState { it.copy(messages = updatedMessages) }
|
|
||||||
|
|
||||||
if (it.senderId != currentUserId) {
|
// 2. If it's a product message and needs details, fetch them
|
||||||
Log.d(TAG, "Marking message as read: ${it.id}")
|
if (incomingChatLine.productId != 0) { // Check if it's a product message
|
||||||
updateMessageStatus(it.id, Constants.STATUS_READ)
|
viewModelScope.launch {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Fetching product detail for ID: ${incomingChatLine.productId}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call your repository function directly
|
||||||
|
val productResponse =
|
||||||
|
chatRepository.fetchProductDetail(incomingChatLine.productId)
|
||||||
|
|
||||||
|
if (productResponse != null && productResponse.product != null) {
|
||||||
|
val fetchedProduct =
|
||||||
|
productResponse.product // Access the nested product object
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Successfully fetched product: ${fetchedProduct.productName}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a complete ProductInfo object
|
||||||
|
val fullProductInfo = ProductInfo(
|
||||||
|
productId = fetchedProduct.productId,
|
||||||
|
productName = fetchedProduct.productName, // Use productName from fetched data
|
||||||
|
productPrice = fetchedProduct.price, // Use productPrice from fetched data
|
||||||
|
productImage = fetchedProduct.image, // Use productImage from fetched data
|
||||||
|
productRating = fetchedProduct.rating.toFloat(),
|
||||||
|
storeName = fetchedProduct.productName // Use storeName from fetched data
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- PHASE 3: Second UI update (fill in full product info) ---
|
||||||
|
_state.update { currentState ->
|
||||||
|
val updatedMessages = currentState.messages.map { msg ->
|
||||||
|
if (msg.id == incomingChatLine.id) {
|
||||||
|
// Found the message, update its productInfo with full details
|
||||||
|
msg.copy(productInfo = fullProductInfo)
|
||||||
|
} else {
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentState.copy(messages = updatedMessages)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Failed to fetch product detail for ID ${incomingChatLine.productId} or product data is null."
|
||||||
|
)
|
||||||
|
// Optionally, update message status to indicate error in product loading
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// // Your existing logic for clearing typing status etc.
|
||||||
|
// if (incomingChatLine.isTyping == false && incomingChatLine.from?.id != sessionManager.getUserId()?.toIntOrNull()) {
|
||||||
|
// _state.update { it.copy(isOtherUserTyping = false) }
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,10 +367,10 @@ class ChatViewModel @Inject constructor(
|
|||||||
if (roomId <= 0) {
|
if (roomId <= 0) {
|
||||||
Log.e(TAG, "Cannot join room: Invalid room ID")
|
Log.e(TAG, "Cannot join room: Invalid room ID")
|
||||||
return
|
return
|
||||||
|
} else if (roomId > 0){
|
||||||
|
Log.d(TAG, "Joining socket room: $roomId")
|
||||||
|
socketService.joinRoom(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Joining socket room: $roomId")
|
|
||||||
socketService.joinRoom()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendTypingStatus(isTyping: Boolean) {
|
fun sendTypingStatus(isTyping: Boolean) {
|
||||||
@ -313,10 +439,13 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getChatList() {
|
fun getChatList() {
|
||||||
|
_isLoading.value = true
|
||||||
Log.d(TAG, "Getting chat list...")
|
Log.d(TAG, "Getting chat list...")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_chatList.value = Result.Loading
|
// _chatList.value = Result.Loading
|
||||||
_chatList.value = chatRepository.getListChat()
|
_chatList.value = chatRepository.getListChat()
|
||||||
|
_isLoading.value = false
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,7 +824,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//update message status
|
//update message status
|
||||||
fun updateMessageStatus(messageId: Int, status: String) {
|
fun updateMessageStatus(messageId: Int, status: String) {
|
||||||
Log.d(TAG, "Updating message status - ID: $messageId, Status: $status")
|
Log.d(TAG, "Updating message status - ID: $messageId, Status: $status")
|
||||||
|
|
||||||
@ -723,13 +852,26 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//set image attachment
|
//set image attachment
|
||||||
fun setSelectedImageFile(file: File?) {
|
fun setSelectedImageFile(file: File?) {
|
||||||
selectedImageFile = file
|
selectedImageFile = file
|
||||||
updateState { it.copy(hasAttachment = file != null) }
|
updateState { it.copy(hasAttachment = file != null) }
|
||||||
Log.d(TAG, "Image attachment ${if (file != null) "selected: ${file.name}" else "cleared"}")
|
Log.d(TAG, "Image attachment ${if (file != null) "selected: ${file.name}" else "cleared"}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearSelectedImage() {
|
||||||
|
Log.d(TAG, "Clearing selected image attachment")
|
||||||
|
|
||||||
|
selectedImageFile?.let { file ->
|
||||||
|
Log.d(TAG, "Clearing image file: ${file.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedImageFile = null
|
||||||
|
updateState { it.copy(hasAttachment = false) }
|
||||||
|
|
||||||
|
Log.d(TAG, "Image attachment cleared successfully")
|
||||||
|
}
|
||||||
|
|
||||||
// convert form chatLine api to UI chat messages
|
// convert form chatLine api to UI chat messages
|
||||||
private fun convertChatLineToUiMessage(chatLine: ChatLine): ChatUiMessage {
|
private fun convertChatLineToUiMessage(chatLine: ChatLine): ChatUiMessage {
|
||||||
val formattedTime = formatTimestamp(chatLine.createdAt)
|
val formattedTime = formatTimestamp(chatLine.createdAt)
|
||||||
@ -745,7 +887,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert chat history item to ui
|
// convert chat history item to ui
|
||||||
private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
|
private fun convertChatLineToUiMessageHistory(chatItem: ChatItem): ChatUiMessage {
|
||||||
val formattedTime = formatTimestamp(chatItem.createdAt)
|
val formattedTime = formatTimestamp(chatItem.createdAt)
|
||||||
|
|
||||||
@ -886,7 +1028,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//format price
|
//format price
|
||||||
private fun formatPrice(price: String): String {
|
private fun formatPrice(price: String): String {
|
||||||
return if (price.startsWith("Rp")) price else "Rp$price"
|
return if (price.startsWith("Rp")) price else "Rp$price"
|
||||||
}
|
}
|
||||||
@ -912,9 +1054,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
|
|
||||||
// helper function to update live data
|
// helper function to update live data
|
||||||
private fun updateState(update: (ChatUiState) -> ChatUiState) {
|
private fun updateState(update: (ChatUiState) -> ChatUiState) {
|
||||||
_state.value?.let {
|
_state.value = update(_state.value)
|
||||||
_state.value = update(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//clear any error messages
|
//clear any error messages
|
||||||
@ -1007,6 +1147,73 @@ class ChatViewModel @Inject constructor(
|
|||||||
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
|
private fun isThisYear(messageCalendar: Calendar, today: Calendar): Boolean {
|
||||||
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
|
return messageCalendar.get(Calendar.YEAR) == today.get(Calendar.YEAR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setChatRoomId(roomId: Int) {
|
||||||
|
_chatRoomId.value = roomId
|
||||||
|
joinSocketRoom(roomId)
|
||||||
|
loadChatHistory(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToUiMessage(chatLine: ChatLine): ChatUiMessage {
|
||||||
|
|
||||||
|
val formattedTime = formatTimestamp(chatLine.createdAt)
|
||||||
|
return ChatUiMessage(
|
||||||
|
id = chatLine.id,
|
||||||
|
message = chatLine.message,
|
||||||
|
attachment = chatLine.attachment,
|
||||||
|
status = chatLine.status,
|
||||||
|
time = formattedTime, // or format from createdAt if needed
|
||||||
|
isSentByMe = chatLine.senderId == currentUserId,
|
||||||
|
messageType = MessageType.TEXT, // or detect from chatLine if needed
|
||||||
|
productInfo = null, // optional, if applicable
|
||||||
|
createdAt = chatLine.createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapChatLineToUiMessage(chatLine: ChatLine, fetchedProductInfo: ProductInfo? = null): ChatUiMessage {
|
||||||
|
val isSentByMe = chatLine.senderId == sessionManager.getUserId()?.toIntOrNull() // Using senderId now
|
||||||
|
val formattedTime = try {
|
||||||
|
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
}
|
||||||
|
val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||||
|
val date = inputFormat.parse(chatLine.createdAt)
|
||||||
|
date?.let { outputFormat.format(it) } ?: ""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ChatViewModel", "Error parsing date: ${chatLine.createdAt}", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine message type based on what ChatLine provides
|
||||||
|
val messageType = when {
|
||||||
|
chatLine.attachment?.isNotEmpty() == true -> MessageType.IMAGE
|
||||||
|
chatLine.productId != 0 -> MessageType.PRODUCT // If productId is non-zero, it's a product message
|
||||||
|
else -> MessageType.TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize productInfo: if fetchedProductInfo is provided, use it.
|
||||||
|
// Otherwise, if ChatLine has a productId, create a ProductInfo with just the ID.
|
||||||
|
// If no productId, it's null.
|
||||||
|
val productInfo = fetchedProductInfo ?: if (chatLine.productId != 0) {
|
||||||
|
// Create a placeholder ProductInfo with just the ID for initial display
|
||||||
|
// The full details will be fetched later
|
||||||
|
ProductInfo(productId = chatLine.productId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChatUiMessage(
|
||||||
|
id = chatLine.id,
|
||||||
|
message = chatLine.message,
|
||||||
|
attachment = chatLine.attachment,
|
||||||
|
status = chatLine.status,
|
||||||
|
time = formattedTime,
|
||||||
|
isSentByMe = isSentByMe,
|
||||||
|
messageType = messageType,
|
||||||
|
productInfo = productInfo, // Use the determined productInfo
|
||||||
|
createdAt = chatLine.createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MessageType {
|
enum class MessageType {
|
||||||
@ -1016,12 +1223,12 @@ enum class MessageType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ProductInfo(
|
data class ProductInfo(
|
||||||
val productId: Int,
|
val productId: Int, // Keep productId here
|
||||||
val productName: String,
|
val productName: String? = null, // Make nullable
|
||||||
val productPrice: String,
|
val productPrice: String? = null, // Make nullable
|
||||||
val productImage: String,
|
val productImage: String? = null, // Make nullable
|
||||||
val productRating: Float,
|
val productRating: Float = 0f, // Default value
|
||||||
val storeName: String
|
val storeName: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
// representing chat messages to UI
|
// representing chat messages to UI
|
||||||
@ -1037,8 +1244,6 @@ data class ChatUiMessage(
|
|||||||
val createdAt: String
|
val createdAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// representing UI state to screen
|
// representing UI state to screen
|
||||||
data class ChatUiState(
|
data class ChatUiState(
|
||||||
val messages: List<ChatUiMessage> = emptyList(),
|
val messages: List<ChatUiMessage> = emptyList(),
|
||||||
@ -1056,4 +1261,8 @@ data class ChatUiState(
|
|||||||
val productImageUrl: String = "",
|
val productImageUrl: String = "",
|
||||||
val productRating: Float = 0f,
|
val productRating: Float = 0f,
|
||||||
val storeName: String = ""
|
val storeName: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//data class ChatUiState(
|
||||||
|
// val messages: List<ChatUiMessage> = emptyList()
|
||||||
|
//)
|
||||||
|
@ -10,14 +10,24 @@ import com.alya.ecommerce_serang.utils.SessionManager
|
|||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.socket.client.IO
|
import io.socket.client.IO
|
||||||
import io.socket.client.Socket
|
import io.socket.client.Socket
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class SocketIOService(
|
@Singleton
|
||||||
|
class SocketIOService @Inject constructor(
|
||||||
private val sessionManager: SessionManager
|
private val sessionManager: SessionManager
|
||||||
) {
|
) {
|
||||||
|
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val TAG = "SocketIOService"
|
private val TAG = "SocketIOService"
|
||||||
|
|
||||||
// Socket.IO client
|
// Socket.IO client
|
||||||
@ -30,8 +40,8 @@ class SocketIOService(
|
|||||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected())
|
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected())
|
||||||
val connectionState: StateFlow<ConnectionState> = _connectionState
|
val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||||
|
|
||||||
private val _newMessages = MutableStateFlow<ChatLine?>(null)
|
private val _newMessages = MutableSharedFlow<ChatLine>(extraBufferCapacity = 1) // Using extraBufferCapacity for a non-suspending emit
|
||||||
val newMessages: StateFlow<ChatLine?> = _newMessages
|
val newMessages: SharedFlow<ChatLine> = _newMessages
|
||||||
|
|
||||||
private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
|
private val _typingStatus = MutableStateFlow<TypingStatus?>(null)
|
||||||
val typingStatus: StateFlow<TypingStatus?> = _typingStatus
|
val typingStatus: StateFlow<TypingStatus?> = _typingStatus
|
||||||
@ -85,63 +95,95 @@ class SocketIOService(
|
|||||||
* Sets up Socket.IO event listeners
|
* Sets up Socket.IO event listeners
|
||||||
*/
|
*/
|
||||||
private fun setupSocketListeners() {
|
private fun setupSocketListeners() {
|
||||||
socket?.let { socket ->
|
|
||||||
// Connection events
|
|
||||||
socket.on(Socket.EVENT_CONNECT) {
|
|
||||||
Log.d(TAG, "Socket.IO connected")
|
|
||||||
isConnected = true
|
|
||||||
_connectionState.value = ConnectionState.Connected
|
|
||||||
_connectionStateLiveData.postValue(ConnectionState.Connected)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on(Socket.EVENT_DISCONNECT) {
|
socket?.on(Constants.EVENT_NEW_MESSAGE) { args -> // Use the event name your server emits
|
||||||
Log.d(TAG, "Socket.IO disconnected")
|
Log.d(TAG, "Raw event received on ${Constants.EVENT_NEW_MESSAGE}: ${args.firstOrNull()}") // Check raw args
|
||||||
isConnected = false
|
|
||||||
_connectionState.value = ConnectionState.Disconnected("Disconnected from server")
|
|
||||||
_connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
|
if (args.isNotEmpty()) {
|
||||||
val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
|
|
||||||
Log.e(TAG, "Socket.IO connection error: $error")
|
|
||||||
isConnected = false
|
|
||||||
_connectionState.value = ConnectionState.Error("Connection error: $error")
|
|
||||||
_connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat events
|
|
||||||
socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
|
|
||||||
try {
|
try {
|
||||||
if (args.isNotEmpty() && args[0] != null) {
|
val messageJson = args[0].toString()
|
||||||
val messageJson = args[0].toString()
|
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
|
||||||
Log.d(TAG, "Received new message: $messageJson")
|
Log.d(TAG, "Successfully parsed ChatLine: ${chatLine.message}")
|
||||||
val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
|
Log.d(TAG, "Emitting new message to _newMessages SharedFlow...") // New log
|
||||||
_newMessages.value = chatLine
|
|
||||||
_newMessagesLiveData.postValue(chatLine)
|
// Use the serviceScope to launch a coroutine for emit()
|
||||||
|
serviceScope.launch {
|
||||||
|
_newMessages.emit(chatLine) // This ensures every message is processed
|
||||||
|
Log.d(TAG, "New message emitted to SharedFlow.") // New log after emit
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error parsing new message event", e)
|
Log.e(TAG, "Error parsing or emitting new message: ${e.message}", e)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on(Constants.EVENT_TYPING) { args ->
|
|
||||||
try {
|
|
||||||
if (args.isNotEmpty() && args[0] != null) {
|
|
||||||
val typingData = args[0] as JSONObject
|
|
||||||
val userId = typingData.getInt("userId")
|
|
||||||
val roomId = typingData.getInt("roomId")
|
|
||||||
val isTyping = typingData.getBoolean("isTyping")
|
|
||||||
|
|
||||||
Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
|
|
||||||
val status = TypingStatus(userId, roomId, isTyping)
|
|
||||||
_typingStatus.value = status
|
|
||||||
_typingStatusLiveData.postValue(status)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Error parsing typing event", e)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Received empty args for ${Constants.EVENT_NEW_MESSAGE}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// socket?.on(Constants.EVENT_NEW_MESSAGE) { args ->
|
||||||
|
// if (args.isNotEmpty()) {
|
||||||
|
// val messageJson = args[0].toString()
|
||||||
|
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
|
||||||
|
// Log.d("SocketIOService", "Message received: ${chatLine.message}")
|
||||||
|
// _newMessages.value = chatLine
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// socket?.let { socket ->
|
||||||
|
// // Connection events
|
||||||
|
// socket.on(Socket.EVENT_CONNECT) {
|
||||||
|
// Log.d(TAG, "Socket.IO connected")
|
||||||
|
// isConnected = true
|
||||||
|
// _connectionState.value = ConnectionState.Connected
|
||||||
|
// _connectionStateLiveData.postValue(ConnectionState.Connected)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// socket.on(Socket.EVENT_DISCONNECT) {
|
||||||
|
// Log.d(TAG, "Socket.IO disconnected")
|
||||||
|
// isConnected = false
|
||||||
|
// _connectionState.value = ConnectionState.Disconnected("Disconnected from server")
|
||||||
|
// _connectionStateLiveData.postValue(ConnectionState.Disconnected("Disconnected from server"))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// socket.on(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||||
|
// val error = if (args.isNotEmpty() && args[0] != null) args[0].toString() else "Unknown error"
|
||||||
|
// Log.e(TAG, "Socket.IO connection error: $error")
|
||||||
|
// isConnected = false
|
||||||
|
// _connectionState.value = ConnectionState.Error("Connection error: $error")
|
||||||
|
// _connectionStateLiveData.postValue(ConnectionState.Error("Connection error: $error"))
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Chat events
|
||||||
|
// socket.on(Constants.EVENT_NEW_MESSAGE) { args ->
|
||||||
|
// try {
|
||||||
|
// if (args.isNotEmpty() && args[0] != null) {
|
||||||
|
// val messageJson = args[0].toString()
|
||||||
|
// Log.d(TAG, "Received new message: $messageJson")
|
||||||
|
// val chatLine = Gson().fromJson(messageJson, ChatLine::class.java)
|
||||||
|
// _newMessages.value = chatLine
|
||||||
|
// _newMessagesLiveData.postValue(chatLine)
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e(TAG, "Error parsing new message event", e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// socket.on(Constants.EVENT_TYPING) { args ->
|
||||||
|
// try {
|
||||||
|
// if (args.isNotEmpty() && args[0] != null) {
|
||||||
|
// val typingData = args[0] as JSONObject
|
||||||
|
// val userId = typingData.getInt("userId")
|
||||||
|
// val roomId = typingData.getInt("roomId")
|
||||||
|
// val isTyping = typingData.getBoolean("isTyping")
|
||||||
|
//
|
||||||
|
// Log.d(TAG, "Received typing status: User $userId in room $roomId is typing: $isTyping")
|
||||||
|
// val status = TypingStatus(userId, roomId, isTyping)
|
||||||
|
// _typingStatus.value = status
|
||||||
|
// _typingStatusLiveData.postValue(status)
|
||||||
|
// }
|
||||||
|
// } catch (e: Exception) {
|
||||||
|
// Log.e(TAG, "Error parsing typing event", e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,22 +201,29 @@ class SocketIOService(
|
|||||||
/**
|
/**
|
||||||
* Joins a specific chat room
|
* Joins a specific chat room
|
||||||
*/
|
*/
|
||||||
fun joinRoom() {
|
fun joinRoom(roomId: Int) {
|
||||||
|
// if (!isConnected) {
|
||||||
|
// connect()
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Get user ID from SessionManager
|
||||||
|
// val userId = sessionManager.getUserId()
|
||||||
|
// if (userId.isNullOrEmpty()) {
|
||||||
|
// Log.e(TAG, "Cannot join room: User ID is null or empty")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Join the room using the current user's ID
|
||||||
|
// socket?.emit("joinRoom", roomId) // ✅
|
||||||
|
// Log.d(TAG, "Joined room ID: $roomId")
|
||||||
|
// Log.d(TAG, "Joined room for user: $userId")
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
connect()
|
connect()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user ID from SessionManager
|
socket?.emit("joinRoom", roomId)
|
||||||
val userId = sessionManager.getUserId()
|
Log.d(TAG, "Joined room ID: $roomId")
|
||||||
if (userId.isNullOrEmpty()) {
|
|
||||||
Log.e(TAG, "Cannot join room: User ID is null or empty")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join the room using the current user's ID
|
|
||||||
socket?.emit("joinRoom", userId)
|
|
||||||
Log.d(TAG, "Joined room for user: $userId")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -208,25 +208,6 @@ class HomeFragment : Fragment() {
|
|||||||
private fun initUi() {
|
private fun initUi() {
|
||||||
// For LightStatusBar
|
// For LightStatusBar
|
||||||
setLightStatusBar()
|
setLightStatusBar()
|
||||||
// val banners = binding.banners
|
|
||||||
// banners.offscreenPageLimit = 1
|
|
||||||
//
|
|
||||||
// val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)
|
|
||||||
// val currentItemHorizontalMarginPx =
|
|
||||||
// resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin)
|
|
||||||
// val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
|
|
||||||
//
|
|
||||||
// banners.setPageTransformer { page, position ->
|
|
||||||
// page.translationX = -pageTranslationX * position
|
|
||||||
// page.scaleY = 1 - (0.25f * kotlin.math.abs(position))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// banners.addItemDecoration(
|
|
||||||
// HorizontalMarginItemDecoration(
|
|
||||||
// requireContext(),
|
|
||||||
// R.dimen.viewpager_current_item_horizontal_margin
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleProductClick(product: ProductsItem) {
|
private fun handleProductClick(product: ProductsItem) {
|
||||||
@ -248,8 +229,4 @@ class HomeFragment : Fragment() {
|
|||||||
categoryAdapter = null
|
categoryAdapter = null
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun showLoading(isLoading: Boolean) {
|
|
||||||
// binding.progressBar.isVisible = isLoading
|
|
||||||
// }
|
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.home
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
@ -65,6 +66,16 @@ class SearchResultsAdapter(
|
|||||||
|
|
||||||
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
val storeName = product.storeId?.let { storeMap[it]?.storeName } ?: "Unknown Store"
|
||||||
binding.tvStoreName.text = storeName
|
binding.tvStoreName.text = storeName
|
||||||
|
val ratingStr = product.rating
|
||||||
|
val ratingValue = ratingStr?.toFloatOrNull()
|
||||||
|
|
||||||
|
if (ratingValue != null && ratingValue > 0f) {
|
||||||
|
binding.rating.text = String.format("%.1f", ratingValue)
|
||||||
|
binding.rating.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.rating.text = "Belum ada rating"
|
||||||
|
binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -110,11 +111,6 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// viewModel.getPaymentMethods { paymentMethods ->
|
|
||||||
// // Logging is just for debugging
|
|
||||||
// Log.d("CheckoutActivity", "Loaded ${paymentMethods.size} payment methods")
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupToolbar() {
|
private fun setupToolbar() {
|
||||||
@ -165,7 +161,7 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
// Observe loading state
|
// Observe loading state
|
||||||
viewModel.isLoading.observe(this) { isLoading ->
|
viewModel.isLoading.observe(this) { isLoading ->
|
||||||
binding.btnPay.isEnabled = !isLoading
|
binding.btnPay.isEnabled = !isLoading
|
||||||
// Show/hide loading indicator if you have one
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe error messages
|
// Observe error messages
|
||||||
@ -273,10 +269,14 @@ class CheckoutActivity : AppCompatActivity() {
|
|||||||
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
|
private fun updateShippingUI(shipName: String, shipService: String, shipEtd: String, shipPrice: Int) {
|
||||||
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
|
if (shipName.isNotEmpty() && shipService.isNotEmpty()) {
|
||||||
// Display shipping name and service in one line
|
// Display shipping name and service in one line
|
||||||
|
binding.cardShipment.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.tvCourierName.text = "$shipName $shipService"
|
binding.tvCourierName.text = "$shipName $shipService"
|
||||||
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
|
binding.tvDeliveryEstimate.text = "$shipEtd hari kerja"
|
||||||
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
|
binding.tvShippingPrice.text = formatCurrency(shipPrice.toDouble())
|
||||||
binding.rbJne.isChecked = true
|
binding.rbJne.isChecked = true
|
||||||
|
} else {
|
||||||
|
binding.cardShipment.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class CheckoutViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
shipPrice = 0, // Will be set when user selects shipping
|
shipPrice = 0, // Will be set when user selects shipping
|
||||||
shipName = "",
|
shipName = "",
|
||||||
shipService = "",
|
shipService = "",
|
||||||
isNego = false, // Default value
|
isNego = false, // Default value
|
||||||
productId = productId,
|
productId = productId,
|
||||||
quantity = quantity,
|
quantity = quantity,
|
||||||
shipEtd = "",
|
shipEtd = "",
|
||||||
|
@ -289,7 +289,7 @@ class AddAddressActivity : AppCompatActivity() {
|
|||||||
val isStoreLocation = false
|
val isStoreLocation = false
|
||||||
|
|
||||||
val provinceId = viewModel.selectedProvinceId
|
val provinceId = viewModel.selectedProvinceId
|
||||||
val cityId = viewModel.selectedCityId
|
val cityId = viewModel.selectedCityId.toString()
|
||||||
|
|
||||||
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
|
Log.d(TAG, "Form data: street=$street, subDistrict=$subDistrict, postalCode=$postalCode, " +
|
||||||
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
|
"recipient=$recipient, phone=$phone, userId=$userId, provinceId=$provinceId, cityId=$cityId, " +
|
||||||
@ -333,18 +333,19 @@ class AddAddressActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
// Create request with all fields
|
// Create request with all fields
|
||||||
val request = CreateAddressRequest(
|
val request = CreateAddressRequest(
|
||||||
|
userId = userId,
|
||||||
lat = latitude!!, // Safe to use !! as we've checked above
|
lat = latitude!!, // Safe to use !! as we've checked above
|
||||||
long = longitude!!,
|
long = longitude!!,
|
||||||
street = street,
|
street = street,
|
||||||
subDistrict = subDistrict,
|
subDistrict = subDistrict,
|
||||||
cityId = cityId,
|
cityId = cityId, // ⚠️ Make sure this is Int
|
||||||
provId = provinceId,
|
provId = provinceId,
|
||||||
postCode = postalCode,
|
postCode = postalCode,
|
||||||
|
idVillage = "", // Or provide a real ID if needed
|
||||||
detailAddress = street,
|
detailAddress = street,
|
||||||
userId = userId,
|
isStoreLocation = false,
|
||||||
recipient = recipient,
|
recipient = recipient,
|
||||||
phone = phone,
|
phone = phone
|
||||||
isStoreLocation = isStoreLocation
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(TAG, "Form validation successful, submitting address: $request")
|
Log.d(TAG, "Form validation successful, submitting address: $request")
|
||||||
|
@ -36,8 +36,8 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
|||||||
get() = savedStateHandle.get<Int>("selectedProvinceId")
|
get() = savedStateHandle.get<Int>("selectedProvinceId")
|
||||||
set(value) { savedStateHandle["selectedProvinceId"] = value }
|
set(value) { savedStateHandle["selectedProvinceId"] = value }
|
||||||
|
|
||||||
var selectedCityId: Int?
|
var selectedCityId: String?
|
||||||
get() = savedStateHandle.get<Int>("selectedCityId")
|
get() = savedStateHandle.get<String>("selectedCityId")
|
||||||
set(value) { savedStateHandle["selectedCityId"] = value }
|
set(value) { savedStateHandle["selectedCityId"] = value }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -129,7 +129,7 @@ class AddAddressViewModel(private val repository: OrderRepository, private val u
|
|||||||
selectedProvinceId = id
|
selectedProvinceId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedCityId(id: Int) {
|
fun updateSelectedCityId(id: String) {
|
||||||
selectedCityId = id
|
selectedCityId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import android.util.Log
|
|||||||
import android.widget.ArrayAdapter
|
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.CitiesItem
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
|
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
|
||||||
|
|
||||||
// UI adapters and helpers
|
// UI adapters and helpers
|
||||||
class ProvinceAdapter(
|
class ProvinceAdapter(
|
||||||
@ -12,6 +14,7 @@ class ProvinceAdapter(
|
|||||||
resource: Int = android.R.layout.simple_dropdown_item_1line
|
resource: Int = android.R.layout.simple_dropdown_item_1line
|
||||||
) : ArrayAdapter<String>(context, resource, ArrayList()) {
|
) : ArrayAdapter<String>(context, resource, ArrayList()) {
|
||||||
|
|
||||||
|
//call from endpoint
|
||||||
private val provinces = ArrayList<ProvincesItem>()
|
private val provinces = ArrayList<ProvincesItem>()
|
||||||
|
|
||||||
fun updateData(newProvinces: List<ProvincesItem>) {
|
fun updateData(newProvinces: List<ProvincesItem>) {
|
||||||
@ -46,7 +49,52 @@ class CityAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCityId(position: Int): Int? {
|
fun getCityId(position: Int): String? {
|
||||||
return cities.getOrNull(position)?.cityId?.toIntOrNull()
|
return cities.getOrNull(position)?.cityId?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,7 +39,6 @@ import okhttp3.MultipartBody
|
|||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.NumberFormat
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -63,7 +62,6 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val paymentMethods = arrayOf(
|
private val paymentMethods = arrayOf(
|
||||||
"Transfer Bank",
|
"Transfer Bank",
|
||||||
"E-Wallet",
|
|
||||||
"QRIS",
|
"QRIS",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -129,7 +127,7 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
val paymentMethods = listOf("Transfer Bank", "COD", "QRIS")
|
val paymentMethods = listOf("Transfer Bank", "QRIS")
|
||||||
val adapter = SpinnerCardAdapter(this, paymentMethods)
|
val adapter = SpinnerCardAdapter(this, paymentMethods)
|
||||||
binding.spinnerPaymentMethod.adapter = adapter
|
binding.spinnerPaymentMethod.adapter = adapter
|
||||||
}
|
}
|
||||||
@ -320,11 +318,12 @@ class AddEvidencePaymentActivity : AppCompatActivity() {
|
|||||||
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Silahkan pilih metode pembayaran", Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
binding.etAccountNumber.visibility = View.GONE
|
||||||
|
|
||||||
if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
|
// if (binding.etAccountNumber.text.toString().trim().isEmpty()) {
|
||||||
Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
|
// Toast.makeText(this, "Silahkan isi nomor rekening/HP", Toast.LENGTH_SHORT).show()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
|
if (binding.tvPaymentDate.text.toString() == "Pilih tanggal") {
|
||||||
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Silahkan pilih tanggal pembayaran", Toast.LENGTH_SHORT).show()
|
||||||
|
@ -10,6 +10,7 @@ import com.alya.ecommerce_serang.data.api.dto.CompletedOrderRequest
|
|||||||
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
|
import com.alya.ecommerce_serang.data.api.response.customer.order.CancelOrderResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
|
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListItemsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.OrderListResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
|
import com.alya.ecommerce_serang.data.api.response.customer.order.Orders
|
||||||
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
|
import com.alya.ecommerce_serang.data.api.response.order.CompletedOrderResponse
|
||||||
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
@ -18,6 +19,13 @@ import com.alya.ecommerce_serang.ui.order.address.ViewState
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -29,8 +37,8 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
private const val TAG = "HistoryViewModel"
|
private const val TAG = "HistoryViewModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>()
|
// private val _orders = MutableLiveData<ViewState<List<OrdersItem>>>()
|
||||||
val orders: LiveData<ViewState<List<OrdersItem>>> = _orders
|
// val orders: LiveData<ViewState<List<OrdersItem>>> = _orders
|
||||||
|
|
||||||
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
|
private val _orderCompletionStatus = MutableLiveData<Result<CompletedOrderResponse>>()
|
||||||
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
|
val orderCompletionStatus: LiveData<Result<CompletedOrderResponse>> = _orderCompletionStatus
|
||||||
@ -59,81 +67,156 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
private val _error = MutableLiveData<String>()
|
private val _error = MutableLiveData<String>()
|
||||||
val error: LiveData<String> get() = _error
|
val error: LiveData<String> get() = _error
|
||||||
|
|
||||||
fun getOrderList(status: String) {
|
private val _selectedStatus = MutableStateFlow("all")
|
||||||
_orders.value = ViewState.Loading
|
val selectedStatus: StateFlow<String> = _selectedStatus.asStateFlow()
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
val orders: StateFlow<ViewState<List<OrdersItem>>> =
|
||||||
if (status == "all") {
|
_selectedStatus
|
||||||
// Get all orders by combining all statuses
|
.flatMapLatest { status ->
|
||||||
getAllOrdersCombined()
|
flow<ViewState<List<OrdersItem>>> {
|
||||||
} else {
|
Log.d(TAG, "⏳ Loading orders for status = $status")
|
||||||
// Get orders for specific status
|
emit(ViewState.Loading)
|
||||||
when (val result = repository.getOrderList(status)) {
|
|
||||||
is Result.Success -> {
|
val viewState =
|
||||||
_orders.value = ViewState.Success(result.data.orders)
|
if (status == "all") {
|
||||||
Log.d(TAG, "Orders loaded successfully: ${result.data.orders.size} items")
|
getAllOrdersCombined().also {
|
||||||
|
Log.d(TAG, "✅ Combined orders size = ${(it as? ViewState.Success)?.data?.size}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (val r = repository.getOrderList(status)) {
|
||||||
|
|
||||||
|
is Result.Loading -> {
|
||||||
|
Log.d(TAG, " repository.getOrderList($status) → Loading")
|
||||||
|
ViewState.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Success -> {
|
||||||
|
Log.d(TAG, "✅ repository.getOrderList($status) success, size = ${r.data.orders.size}")
|
||||||
|
// Tag each order so the fragment’s filter works
|
||||||
|
val tagged = r.data.orders.onEach { it.displayStatus = status }
|
||||||
|
ViewState.Success(tagged)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e(TAG, "❌ repository.getOrderList($status) error = ${r.exception.message}")
|
||||||
|
ViewState.Error(r.exception.message ?: "Unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
|
||||||
_orders.value = ViewState.Error(result.exception.message ?: "Unknown error occurred")
|
emit(viewState)
|
||||||
Log.e(TAG, "Error loading orders", result.exception)
|
|
||||||
}
|
|
||||||
is Result.Loading -> {
|
|
||||||
// Keep loading state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
|
|
||||||
Log.e(TAG, "Exception in getOrderList", e)
|
|
||||||
}
|
}
|
||||||
}
|
.stateIn(
|
||||||
}
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
ViewState.Loading // ② initial value, still fine
|
||||||
|
)
|
||||||
|
|
||||||
private suspend fun getAllOrdersCombined() {
|
|
||||||
try {
|
|
||||||
val allStatuses = listOf("unpaid", "paid", "processed", "shipped", "completed", "canceled")
|
|
||||||
val allOrders = mutableListOf<OrdersItem>()
|
|
||||||
|
|
||||||
// Use coroutineScope to allow launching async blocks
|
// fun getOrderList(status: String) {
|
||||||
coroutineScope {
|
// _orders.value = ViewState.Loading
|
||||||
val deferreds = allStatuses.map { status ->
|
// 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 {
|
async {
|
||||||
when (val result = repository.getOrderList(status)) {
|
when (val r = repository.getOrderList(status)) {
|
||||||
is Result.Success -> {
|
is Result.Success -> r.data.orders.onEach { it.displayStatus = status }
|
||||||
// Tag each order with the status it was fetched from
|
else -> emptyList()
|
||||||
result.data.orders.onEach { it.displayStatus = status }
|
|
||||||
}
|
|
||||||
is Result.Error -> {
|
|
||||||
Log.e(TAG, "Error loading orders for status $status", result.exception)
|
|
||||||
emptyList<OrdersItem>()
|
|
||||||
}
|
|
||||||
is Result.Loading -> emptyList<OrdersItem>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.awaitAll()
|
||||||
// Await all results and combine
|
.flatten()
|
||||||
deferreds.awaitAll().forEach { orders ->
|
|
||||||
allOrders.addAll(orders)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort orders
|
|
||||||
val sortedOrders = allOrders.sortedByDescending { order ->
|
|
||||||
try {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(order.createdAt)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_orders.value = ViewState.Success(sortedOrders)
|
|
||||||
Log.d(TAG, "All orders loaded successfully: ${sortedOrders.size} items")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
_orders.value = ViewState.Error("An unexpected error occurred: ${e.message}")
|
|
||||||
Log.e(TAG, "Exception in getAllOrdersCombined", e)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sorted = all.sortedByDescending { order ->
|
||||||
|
try {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||||
|
.parse(order.createdAt)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewState.Success(sorted)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ViewState.Error("Failed to load orders: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun confirmOrderCompleted(orderId: Int, status: String) {
|
fun confirmOrderCompleted(orderId: Int, status: String) {
|
||||||
@ -209,9 +292,52 @@ class HistoryViewModel(private val repository: OrderRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshOrders(status: String = "all") {
|
// fun refreshOrders(status: String = "all") {
|
||||||
Log.d(TAG, "Refreshing orders with status: $status")
|
// Log.d(TAG, "Refreshing orders with status: $status")
|
||||||
// Don't set Loading here if you want to show current data while refreshing
|
// // Don't set Loading here if you want to show current data while refreshing
|
||||||
getOrderList(status)
|
// getOrderList(status)
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun updateStatus(status: String, forceRefresh: Boolean = false) {
|
||||||
|
Log.d(TAG, "↪️ updateStatus(status = $status, forceRefresh = $forceRefresh)")
|
||||||
|
|
||||||
|
// No‑op guard (optional): skip if user re‑selects same tab and no refresh asked
|
||||||
|
if (_selectedStatus.value == status && !forceRefresh) {
|
||||||
|
Log.d(TAG, "🔸 Status unchanged & forceRefresh = false → skip update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedStatus.value = status
|
||||||
|
Log.d(TAG, "✅ _selectedStatus set to \"$status\"")
|
||||||
|
|
||||||
|
if (forceRefresh) {
|
||||||
|
Log.d(TAG, "🔄 forceRefresh = true → launching refresh()")
|
||||||
|
viewModelScope.launch { refresh(status) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun refresh(status: String) {
|
||||||
|
Log.d(TAG, "⏳ refresh(\"$status\") started")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (status == "all") {
|
||||||
|
Log.d(TAG, "🌐 Calling getAllOrdersCombined()")
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -195,7 +195,7 @@ class OrderHistoryAdapter(
|
|||||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
text = itemView.context.getString(R.string.canceled_order_btn)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
showCancelOrderDialog(order.orderId.toString())
|
||||||
viewModel.refreshOrders()
|
// viewModel.refreshOrders()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// deadlineDate.apply {
|
// deadlineDate.apply {
|
||||||
@ -213,14 +213,15 @@ class OrderHistoryAdapter(
|
|||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = itemView.context.getString(R.string.dl_processed)
|
text = itemView.context.getString(R.string.dl_processed)
|
||||||
}
|
}
|
||||||
btnLeft.apply {
|
// gabisa complaint
|
||||||
visibility = View.VISIBLE
|
// btnLeft.apply {
|
||||||
text = itemView.context.getString(R.string.canceled_order_btn)
|
// visibility = View.VISIBLE
|
||||||
setOnClickListener {
|
// text = itemView.context.getString(R.string.canceled_order_btn)
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
// setOnClickListener {
|
||||||
viewModel.refreshOrders()
|
// showCancelOrderDialog(order.orderId.toString())
|
||||||
}
|
// viewModel.refreshOrders()
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
"shipped" -> {
|
"shipped" -> {
|
||||||
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
|
// Untuk status shipped, tampilkan "Lacak Pengiriman" dan "Terima Barang"
|
||||||
@ -237,7 +238,7 @@ class OrderHistoryAdapter(
|
|||||||
text = itemView.context.getString(R.string.claim_complaint)
|
text = itemView.context.getString(R.string.claim_complaint)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
showCancelOrderDialog(order.orderId.toString())
|
showCancelOrderDialog(order.orderId.toString())
|
||||||
viewModel.refreshOrders()
|
// viewModel.refreshOrders()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btnRight.apply {
|
btnRight.apply {
|
||||||
@ -248,7 +249,7 @@ class OrderHistoryAdapter(
|
|||||||
|
|
||||||
// Call ViewModel
|
// Call ViewModel
|
||||||
viewModel.confirmOrderCompleted(order.orderId, "completed")
|
viewModel.confirmOrderCompleted(order.orderId, "completed")
|
||||||
viewModel.refreshOrders()
|
// viewModel.refreshOrders()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,13 +269,21 @@ class OrderHistoryAdapter(
|
|||||||
text = itemView.context.getString(R.string.dl_shipped)
|
text = itemView.context.getString(R.string.dl_shipped)
|
||||||
}
|
}
|
||||||
btnRight.apply {
|
btnRight.apply {
|
||||||
visibility = View.VISIBLE
|
val checkReview = order.orderItems[0].reviewId
|
||||||
text = itemView.context.getString(R.string.add_review)
|
if (checkReview > 0){
|
||||||
setOnClickListener {
|
visibility = View.VISIBLE
|
||||||
addReviewProduct(order)
|
text = itemView.context.getString(R.string.add_review)
|
||||||
viewModel.refreshOrders()
|
setOnClickListener {
|
||||||
// Handle click event
|
|
||||||
|
addReviewProduct(order)
|
||||||
|
// viewModel.refreshOrders()
|
||||||
|
// Handle click event
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
deadlineDate.apply {
|
deadlineDate.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@ -518,7 +527,7 @@ class OrderHistoryAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and show the bottom sheet using the obtained FragmentManager
|
// cancel sebelum bayar
|
||||||
val bottomSheet = CancelOrderBottomSheet(
|
val bottomSheet = CancelOrderBottomSheet(
|
||||||
orderId = orderId,
|
orderId = orderId,
|
||||||
onOrderCancelled = {
|
onOrderCancelled = {
|
||||||
@ -531,6 +540,7 @@ class OrderHistoryAdapter(
|
|||||||
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
|
bottomSheet.show(fragmentActivity.supportFragmentManager, CancelOrderBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tambah review / ulasan
|
||||||
private fun addReviewProduct(order: OrdersItem) {
|
private fun addReviewProduct(order: OrdersItem) {
|
||||||
// Use ViewModel to fetch order details
|
// Use ViewModel to fetch order details
|
||||||
viewModel.getOrderDetails(order.orderId)
|
viewModel.getOrderDetails(order.orderId)
|
||||||
@ -550,7 +560,7 @@ class OrderHistoryAdapter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe the order details result
|
// Observe order items
|
||||||
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
|
viewModel.orderItems.observe(itemView.findViewTreeLifecycleOwner()!!) { orderItems ->
|
||||||
if (orderItems != null && orderItems.isNotEmpty()) {
|
if (orderItems != null && orderItems.isNotEmpty()) {
|
||||||
// For single item review
|
// For single item review
|
||||||
|
@ -5,8 +5,13 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding
|
import com.alya.ecommerce_serang.databinding.FragmentOrderHistoryBinding
|
||||||
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
|
||||||
@ -16,6 +21,12 @@ class OrderHistoryFragment : Fragment() {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
private val historyVm: HistoryViewModel by activityViewModels {
|
||||||
|
BaseViewModelFactory {
|
||||||
|
val api = ApiConfig.getApiService(SessionManager(requireContext()))
|
||||||
|
HistoryViewModel(OrderRepository(api))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var viewPagerAdapter: OrderViewPagerAdapter
|
private lateinit var viewPagerAdapter: OrderViewPagerAdapter
|
||||||
|
|
||||||
@ -33,6 +44,8 @@ class OrderHistoryFragment : Fragment() {
|
|||||||
sessionManager = SessionManager(requireContext())
|
sessionManager = SessionManager(requireContext())
|
||||||
|
|
||||||
setupViewPager()
|
setupViewPager()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViewPager() {
|
private fun setupViewPager() {
|
||||||
@ -53,6 +66,16 @@ class OrderHistoryFragment : Fragment() {
|
|||||||
else -> "Tab $position"
|
else -> "Tab $position"
|
||||||
}
|
}
|
||||||
}.attach()
|
}.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 re‑query */
|
||||||
|
historyVm.updateStatus(status, forceRefresh = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
@ -8,8 +8,10 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
import com.alya.ecommerce_serang.data.api.dto.OrdersItem
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
@ -27,17 +29,26 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private lateinit var sessionManager: SessionManager
|
private lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
private val viewModel: HistoryViewModel by viewModels {
|
private val viewModel: HistoryViewModel by activityViewModels {
|
||||||
BaseViewModelFactory {
|
BaseViewModelFactory {
|
||||||
val apiService = ApiConfig.getApiService(sessionManager)
|
val api = ApiConfig.getApiService(SessionManager(requireContext()))
|
||||||
val orderRepository = OrderRepository(apiService)
|
HistoryViewModel(OrderRepository(api))
|
||||||
HistoryViewModel(orderRepository)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var orderAdapter: OrderHistoryAdapter
|
private lateinit var orderAdapter: OrderHistoryAdapter
|
||||||
|
|
||||||
private var status: String = "all"
|
private var status: String = "all"
|
||||||
|
|
||||||
|
private val detailOrderLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
/* force‑refresh the current tab */
|
||||||
|
viewModel.updateStatus(status, forceRefresh = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_STATUS = "status"
|
private const val ARG_STATUS = "status"
|
||||||
|
|
||||||
@ -73,8 +84,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
observeOrderList()
|
observeOrderList()
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
observeOrderCompletionStatus()
|
// observeOrderCompletionStatus()
|
||||||
loadOrders()
|
// loadOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
@ -96,27 +107,50 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
|
|
||||||
private fun observeOrderList() {
|
private fun observeOrderList() {
|
||||||
// Now we only need to observe one LiveData for all cases
|
// Now we only need to observe one LiveData for all cases
|
||||||
viewModel.orders.observe(viewLifecycleOwner) { result ->
|
// viewModel.orders.observe(viewLifecycleOwner) { result ->
|
||||||
when (result) {
|
// when (result) {
|
||||||
is ViewState.Success -> {
|
// is ViewState.Success -> {
|
||||||
binding.progressBar.visibility = View.GONE
|
// binding.progressBar.visibility = View.GONE
|
||||||
|
//
|
||||||
if (result.data.isNullOrEmpty()) {
|
// if (result.data.isNullOrEmpty()) {
|
||||||
binding.tvEmptyState.visibility = View.VISIBLE
|
// binding.tvEmptyState.visibility = View.VISIBLE
|
||||||
binding.rvOrders.visibility = View.GONE
|
// binding.rvOrders.visibility = View.GONE
|
||||||
} else {
|
// } else {
|
||||||
binding.tvEmptyState.visibility = View.GONE
|
// binding.tvEmptyState.visibility = View.GONE
|
||||||
binding.rvOrders.visibility = View.VISIBLE
|
// binding.rvOrders.visibility = View.VISIBLE
|
||||||
orderAdapter.submitList(result.data)
|
// 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.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.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,51 +158,78 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
|
|
||||||
private fun observeViewModel() {
|
private fun observeViewModel() {
|
||||||
// Observe order completion
|
// 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 ->
|
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(),
|
||||||
loadOrders() // Refresh here
|
"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()
|
|
||||||
}
|
|
||||||
is Result.Loading -> {
|
|
||||||
// Show loading if needed
|
|
||||||
}
|
}
|
||||||
|
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 ->
|
viewModel.cancelOrderStatus.observe(viewLifecycleOwner) { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
Toast.makeText(requireContext(), "Order cancelled successfully!", Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(),
|
||||||
loadOrders() // Refresh here
|
"Order cancelled!", Toast.LENGTH_SHORT).show()
|
||||||
}
|
viewModel.updateStatus(status, forceRefresh = true)
|
||||||
is Result.Error -> {
|
|
||||||
Toast.makeText(requireContext(), "Failed to cancel: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
is Result.Loading -> {
|
|
||||||
// Show loading if needed
|
|
||||||
}
|
}
|
||||||
|
is Result.Error ->
|
||||||
|
Toast.makeText(requireContext(),
|
||||||
|
"Failed: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
else -> { /* Loading */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOrders() {
|
// private fun loadOrders() {
|
||||||
// Simple - just call getOrderList for any status including "all"
|
// // Simple - just call getOrderList for any status including "all"
|
||||||
viewModel.getOrderList(status)
|
// viewModel.getOrderList(status)
|
||||||
}
|
// }
|
||||||
|
|
||||||
private val detailOrderLauncher = registerForActivityResult(
|
// private val detailOrderLauncher = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
// ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
// ) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
// if (result.resultCode == Activity.RESULT_OK) {
|
||||||
// Refresh order list when returning with OK result
|
// // Refresh order list when returning with OK result
|
||||||
loadOrders()
|
//// loadOrders()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
private fun navigateToOrderDetail(order: OrdersItem) {
|
private fun navigateToOrderDetail(order: OrdersItem) {
|
||||||
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
|
val intent = Intent(requireContext(), DetailOrderStatusActivity::class.java).apply {
|
||||||
@ -183,7 +244,9 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
|
override fun onOrderCancelled(orderId: String, success: Boolean, message: String) {
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||||
loadOrders() // Refresh the list
|
// loadOrders() // Refresh the list
|
||||||
|
if (success) viewModel.updateStatus(status, forceRefresh = true)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@ -192,7 +255,8 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
|
override fun onOrderCompleted(orderId: Int, success: Boolean, message: String) {
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||||
loadOrders() // Refresh the list
|
// loadOrders() // Refresh the list
|
||||||
|
if (success) viewModel.updateStatus(status, forceRefresh = true)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@ -207,20 +271,20 @@ class OrderListFragment : Fragment(), OrderHistoryAdapter.OrderActionCallbacks {
|
|||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeOrderCompletionStatus() {
|
// private fun observeOrderCompletionStatus() {
|
||||||
viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
|
// viewModel.orderCompletionStatus.observe(viewLifecycleOwner) { result ->
|
||||||
when (result) {
|
// when (result) {
|
||||||
is Result.Loading -> {
|
// is Result.Loading -> {
|
||||||
// Handle loading state if needed
|
// // Handle loading state if needed
|
||||||
}
|
// }
|
||||||
is Result.Success -> {
|
// is Result.Success -> {
|
||||||
Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
|
// Toast.makeText(requireContext(), "Order completed successfully!", Toast.LENGTH_SHORT).show()
|
||||||
loadOrders()
|
//// loadOrders()
|
||||||
}
|
// }
|
||||||
is Result.Error -> {
|
// is Result.Error -> {
|
||||||
Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
// Toast.makeText(requireContext(), "Failed to complete order: ${result.exception.message}", Toast.LENGTH_SHORT).show()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
@ -9,7 +9,7 @@ class OrderViewPagerAdapter(
|
|||||||
) : FragmentStateAdapter(fragmentActivity) {
|
) : FragmentStateAdapter(fragmentActivity) {
|
||||||
|
|
||||||
// Define all possible order statuses
|
// Define all possible order statuses
|
||||||
private val orderStatuses = listOf(
|
val orderStatuses = listOf(
|
||||||
"all", // All orders
|
"all", // All orders
|
||||||
"unpaid", // Menunggu Tagihan
|
"unpaid", // Menunggu Tagihan
|
||||||
"paid", // Belum Dibayar
|
"paid", // Belum Dibayar
|
||||||
|
@ -55,7 +55,7 @@ class CancelOrderBottomSheet(
|
|||||||
val btnConfirm = view.findViewById<Button>(R.id.btn_confirm)
|
val btnConfirm = view.findViewById<Button>(R.id.btn_confirm)
|
||||||
|
|
||||||
// Set the title
|
// Set the title
|
||||||
tvTitle.text = "Cancel Order #$orderId"
|
tvTitle.text = "Batalkan Pesanan #$orderId"
|
||||||
|
|
||||||
// Set up the spinner with cancellation reasons
|
// Set up the spinner with cancellation reasons
|
||||||
setupReasonSpinner(spinnerReason)
|
setupReasonSpinner(spinnerReason)
|
||||||
@ -94,11 +94,11 @@ class CancelOrderBottomSheet(
|
|||||||
private fun getCancellationReasons(): List<CancelOrderReq> {
|
private fun getCancellationReasons(): List<CancelOrderReq> {
|
||||||
// These should ideally come from the server or a configuration
|
// These should ideally come from the server or a configuration
|
||||||
return listOf(
|
return listOf(
|
||||||
CancelOrderReq(1, "Changed my mind"),
|
CancelOrderReq(1, "Berubah pikiran"),
|
||||||
CancelOrderReq(2, "Found a better option"),
|
CancelOrderReq(2, "Menemukan pilihan yang lebih baik"),
|
||||||
CancelOrderReq(3, "Ordered by mistake"),
|
CancelOrderReq(3, "Kesalahan pemesanan"),
|
||||||
CancelOrderReq(4, "Delivery time too long"),
|
CancelOrderReq(4, "Waktu pengiriman lama"),
|
||||||
CancelOrderReq(5, "Other reason")
|
CancelOrderReq(5, "Lainnya")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,9 @@ import com.google.gson.Gson
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
@ -197,12 +200,12 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
|
Log.d(TAG, "populateOrderDetails: Payment method=${orders.payInfoName ?: "Tidak tersedia"}")
|
||||||
|
|
||||||
// Set subtotal, shipping cost, and total
|
// Set subtotal, shipping cost, and total
|
||||||
val subtotal = orders.totalAmount?.minus(orders.shipmentPrice.toIntOrNull() ?: 0) ?: 0
|
// val subtotal = orders.totalAmount?.minus(orders.shipmentPrice.toDouble() ?: 0) ?: 0
|
||||||
binding.tvSubtotal.text = formatCurrency(subtotal.toDouble())
|
// binding.tvSubtotal.text = formatCurrency(subtotal.toDouble())
|
||||||
binding.tvShippingCost.text = formatCurrency(orders.shipmentPrice.toDouble())
|
binding.tvShippingCost.text = formatCurrency(orders.shipmentPrice.toDouble())
|
||||||
binding.tvTotal.text = formatCurrency(orders.totalAmount?.toDouble() ?: 0.00)
|
binding.tvTotal.text = formatCurrency(orders.totalAmount?.toDouble() ?: 0.00)
|
||||||
|
|
||||||
Log.d(TAG, "populateOrderDetails: Subtotal=$subtotal, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}")
|
Log.d(TAG, "populateOrderDetails: Subtotal=, Shipping=${orders.shipmentPrice}, Total=${orders.totalAmount}")
|
||||||
|
|
||||||
// Adjust buttons based on order status
|
// Adjust buttons based on order status
|
||||||
Log.d(TAG, "populateOrderDetails: Adjusting buttons for status=$orderStatus")
|
Log.d(TAG, "populateOrderDetails: Adjusting buttons for status=$orderStatus")
|
||||||
@ -223,6 +226,11 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
this.adapter = adapter
|
this.adapter = adapter
|
||||||
}
|
}
|
||||||
adapter.submitList(orderItems)
|
adapter.submitList(orderItems)
|
||||||
|
|
||||||
|
// get data from ordetlistitemsitem untuk ambil subtotal nya dan dijumlahkan
|
||||||
|
val subtotalSum = orderItems.sumOf { it.subtotal }
|
||||||
|
binding.tvSubtotal.text = formatCurrency(subtotalSum.toDouble())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun adjustButtonsBasedOnStatus(orders: Orders, status: String) {
|
private fun adjustButtonsBasedOnStatus(orders: Orders, status: String) {
|
||||||
@ -287,20 +295,20 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
// Show status note
|
// Show status note
|
||||||
binding.tvStatusHeader.text = "Sudah Dibayar"
|
binding.tvStatusHeader.text = "Sudah Dibayar"
|
||||||
binding.tvStatusNote.visibility = View.VISIBLE
|
binding.tvStatusNote.visibility = View.VISIBLE
|
||||||
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePay(orders.updatedAt)}"
|
binding.tvStatusNote.text = "Menunggu pesanan dikonfirmasi penjual ${formatDatePaid(orders.updatedAt)}"
|
||||||
binding.tvPaymentDeadlineLabel.text = "Batas konfirmasi penjual:"
|
binding.tvPaymentDeadlineLabel.text = "Batas konfirmasi penjual:"
|
||||||
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
|
binding.tvPaymentDeadline.text = formatDatePaid(orders.updatedAt)
|
||||||
|
|
||||||
// Set buttons
|
// cancel pesanan
|
||||||
binding.btnSecondary.apply {
|
// binding.btnSecondary.apply {
|
||||||
visibility = View.VISIBLE
|
// visibility = View.VISIBLE
|
||||||
text = "Batalkan Pesanan"
|
// text = "Batalkan Pesanan"
|
||||||
setOnClickListener {
|
// setOnClickListener {
|
||||||
Log.d(TAG, "Cancel Order button clicked")
|
// Log.d(TAG, "Cancel Order button clicked")
|
||||||
showCancelOrderDialog(orders.orderId.toString())
|
// showCancelOrderDialog(orders.orderId.toString())
|
||||||
viewModel.getOrderDetails(orders.orderId)
|
// viewModel.getOrderDetails(orders.orderId)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
"processed" -> {
|
"processed" -> {
|
||||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
|
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for processed order")
|
||||||
@ -309,7 +317,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
binding.tvStatusNote.visibility = View.VISIBLE
|
binding.tvStatusNote.visibility = View.VISIBLE
|
||||||
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
|
binding.tvStatusNote.text = "Penjual sedang memproses pesanan Anda"
|
||||||
binding.tvPaymentDeadlineLabel.text = "Batas diproses penjual:"
|
binding.tvPaymentDeadlineLabel.text = "Batas diproses penjual:"
|
||||||
binding.tvPaymentDeadline.text = formatDatePay(orders.updatedAt)
|
binding.tvPaymentDeadline.text = formatDateProcessed(orders.updatedAt)
|
||||||
|
|
||||||
binding.btnSecondary.apply {
|
binding.btnSecondary.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@ -333,7 +341,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
binding.tvStatusNote.visibility = View.VISIBLE
|
binding.tvStatusNote.visibility = View.VISIBLE
|
||||||
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
|
binding.tvStatusNote.text = "Pesanan Anda sedang dalam perjalanan. Akan sampai sekitar ${formatShipmentDate(orders.updatedAt, orders.etd ?: "0")}"
|
||||||
binding.tvPaymentDeadlineLabel.text = "Estimasi pesanan sampai:"
|
binding.tvPaymentDeadlineLabel.text = "Estimasi pesanan sampai:"
|
||||||
binding.tvPaymentDeadline.text = formatShipmentDate(orders.updatedAt, orders.etd ?: "0")
|
binding.tvPaymentDeadline.text = formatShipmentDate(orders.autoCompletedAt, orders.etd ?: "0")
|
||||||
|
|
||||||
binding.btnSecondary.apply {
|
binding.btnSecondary.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@ -367,7 +375,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
binding.tvStatusHeader.text = "Pesanan Selesai"
|
binding.tvStatusHeader.text = "Pesanan Selesai"
|
||||||
binding.tvStatusNote.visibility = View.GONE
|
binding.tvStatusNote.visibility = View.GONE
|
||||||
binding.tvPaymentDeadlineLabel.text = "Pesanan selesai:"
|
binding.tvPaymentDeadlineLabel.text = "Pesanan selesai:"
|
||||||
binding.tvPaymentDeadline.text = formatDate(orders.autoCompletedAt.toString())
|
binding.tvPaymentDeadline.text = formatDate(orders.updatedAt.toString())
|
||||||
|
|
||||||
binding.btnPrimary.apply {
|
binding.btnPrimary.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@ -386,7 +394,7 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
"canceled" -> {
|
"canceled" -> {
|
||||||
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
|
Log.d(TAG, "adjustButtonsBasedOnStatus: Setting up UI for canceled order")
|
||||||
|
|
||||||
binding.tvStatusHeader.text = "Pesanan Selesai"
|
binding.tvStatusHeader.text = "Pesanan Dibatalkan"
|
||||||
binding.tvStatusNote.visibility = View.VISIBLE
|
binding.tvStatusNote.visibility = View.VISIBLE
|
||||||
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
|
binding.tvStatusNote.text = "Pesanan dibatalkan: ${orders.cancelReason ?: "Alasan tidak diberikan"}"
|
||||||
binding.tvPaymentDeadlineLabel.text = "Tanggal dibatalkan: "
|
binding.tvPaymentDeadlineLabel.text = "Tanggal dibatalkan: "
|
||||||
@ -598,10 +606,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
val bottomSheet = CancelOrderBottomSheet(
|
val bottomSheet = CancelOrderBottomSheet(
|
||||||
orderId = orderId,
|
orderId = orderId,
|
||||||
onOrderCancelled = {
|
onOrderCancelled = {
|
||||||
// Handle the successful cancellation
|
|
||||||
// Refresh the data
|
|
||||||
|
|
||||||
// Show a success message
|
|
||||||
Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Order cancelled successfully", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -610,32 +614,17 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDate(dateString: String): String {
|
private fun formatDate(dateString: String): String {
|
||||||
Log.d(TAG, "formatDate: Formatting date: $dateString")
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
val jakarta = ZoneId.of("Asia/Jakarta")
|
||||||
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
|
val instant = Instant.parse(dateString) // parses ISO‑8601 with ‘Z’
|
||||||
|
val zoned = instant.atZone(jakarta)
|
||||||
|
|
||||||
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
|
val time = DateTimeFormatter.ofPattern("HH:mm", Locale("id", "ID")).format(zoned)
|
||||||
val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale("id", "ID"))
|
val date = DateTimeFormatter.ofPattern("dd MMMM yyyy",Locale("id", "ID")).format(zoned)
|
||||||
|
|
||||||
val date = inputFormat.parse(dateString)
|
"$time\n$date"
|
||||||
|
|
||||||
date?.let {
|
|
||||||
val calendar = Calendar.getInstance()
|
|
||||||
calendar.time = it
|
|
||||||
calendar.set(Calendar.HOUR_OF_DAY, 23)
|
|
||||||
calendar.set(Calendar.MINUTE, 59)
|
|
||||||
|
|
||||||
val timePart = timeFormat.format(calendar.time)
|
|
||||||
val datePart = dateFormat.format(calendar.time)
|
|
||||||
|
|
||||||
val formatted = "$timePart\n$datePart"
|
|
||||||
Log.d(TAG, "formatDate: Formatted date: $formatted")
|
|
||||||
formatted
|
|
||||||
} ?: dateString
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "formatDate: Error formatting date: ${e.message}", e)
|
Log.e(TAG, "formatDate: $e")
|
||||||
dateString
|
dateString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -673,6 +662,73 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatDatePaid(dateString: String): String {
|
||||||
|
Log.d(TAG, "formatDatePay: Formatting payment date: $dateString")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Parse the ISO 8601 date
|
||||||
|
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||||
|
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
|
||||||
|
val createdDate = isoDateFormat.parse(dateString)
|
||||||
|
|
||||||
|
// Add 24 hours to get due date
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.time = createdDate
|
||||||
|
calendar.add(Calendar.HOUR, 120)
|
||||||
|
val dueDate = calendar.time
|
||||||
|
|
||||||
|
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
|
||||||
|
|
||||||
|
val timePart = timeFormat.format(dueDate)
|
||||||
|
val datePart = dateFormat.format(dueDate)
|
||||||
|
|
||||||
|
val formatted = "$timePart\n$datePart"
|
||||||
|
|
||||||
|
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
|
||||||
|
formatted
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "formatDatePay: Error formatting date: ${e.message}", e)
|
||||||
|
dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//format batas tgl diproses
|
||||||
|
private fun formatDateProcessed(dateString: String): String {
|
||||||
|
Log.d(TAG, "formatDatePay: Formatting payment date: $dateString")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Parse the ISO 8601 date
|
||||||
|
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||||
|
isoDateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
|
||||||
|
val createdDate = isoDateFormat.parse(dateString)
|
||||||
|
|
||||||
|
// Add 24 hours to get due date
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
calendar.time = createdDate
|
||||||
|
calendar.add(Calendar.HOUR, 72)
|
||||||
|
val dueDate = calendar.time
|
||||||
|
|
||||||
|
val timeFormat = SimpleDateFormat("HH:mm", Locale("id", "ID"))
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale("id", "ID"))
|
||||||
|
|
||||||
|
val timePart = timeFormat.format(dueDate)
|
||||||
|
val datePart = dateFormat.format(dueDate)
|
||||||
|
|
||||||
|
val formatted = "$timePart\n$datePart"
|
||||||
|
|
||||||
|
Log.d(TAG, "formatDatePay: Formatted payment date: $formatted")
|
||||||
|
formatted
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "formatDatePay: Error formatting date: ${e.message}", e)
|
||||||
|
dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatShipmentDate(dateString: String, estimateString: String): String {
|
private fun formatShipmentDate(dateString: String, estimateString: String): String {
|
||||||
Log.d(TAG, "formatShipmentDate: Formatting shipment date: $dateString with ETD: $estimateString")
|
Log.d(TAG, "formatShipmentDate: Formatting shipment date: $dateString with ETD: $estimateString")
|
||||||
|
|
||||||
@ -696,7 +752,6 @@ class DetailOrderStatusActivity : AppCompatActivity() {
|
|||||||
calendar.time = it
|
calendar.time = it
|
||||||
|
|
||||||
// Add estimated days
|
// Add estimated days
|
||||||
calendar.add(Calendar.DAY_OF_MONTH, estimate)
|
|
||||||
val formatted = outputFormat.format(calendar.time)
|
val formatted = outputFormat.format(calendar.time)
|
||||||
|
|
||||||
Log.d(TAG, "formatShipmentDate: Estimated arrival date: $formatted")
|
Log.d(TAG, "formatShipmentDate: Estimated arrival date: $formatted")
|
||||||
|
@ -75,6 +75,7 @@ class DetailOrderViewModel(private val orderRepository: OrderRepository): ViewMo
|
|||||||
orderRepository.submitComplaint(orderId.toString(), reason, imageFile)
|
orderRepository.submitComplaint(orderId.toString(), reason, imageFile)
|
||||||
_isSuccess.value = true
|
_isSuccess.value = true
|
||||||
_message.value = "Order canceled successfully"
|
_message.value = "Order canceled successfully"
|
||||||
|
Log.d("DetailOrderViewModel", "Complaint order success")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_isSuccess.value = false
|
_isSuccess.value = false
|
||||||
|
@ -164,6 +164,7 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//info toko
|
||||||
private fun updateStoreInfo(store: StoreItem?) {
|
private fun updateStoreInfo(store: StoreItem?) {
|
||||||
store?.let {
|
store?.let {
|
||||||
binding.tvSellerName.text = it.storeName
|
binding.tvSellerName.text = it.storeName
|
||||||
@ -230,9 +231,8 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun updateUI(product: Product){
|
private fun updateUI(product: Product){
|
||||||
binding.tvProductName.text = product.productName
|
binding.tvProductName.text = product.productName
|
||||||
binding.tvPrice.text = "Rp${formatCurrency(product.price.toDouble())}"
|
binding.tvPrice.text = formatCurrency(product.price.toDouble())
|
||||||
binding.tvSold.text = "Terjual ${product.totalSold} buah"
|
binding.tvSold.text = "Terjual ${product.totalSold} buah"
|
||||||
binding.tvRating.text = product.rating
|
|
||||||
binding.tvWeight.text = "${product.weight} gram"
|
binding.tvWeight.text = "${product.weight} gram"
|
||||||
binding.tvStock.text = "${product.stock} buah"
|
binding.tvStock.text = "${product.stock} buah"
|
||||||
binding.tvCategory.text = product.productCategory
|
binding.tvCategory.text = product.productCategory
|
||||||
@ -243,7 +243,7 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
isWholesaleSelected = false // Default to regular pricing
|
isWholesaleSelected = false // Default to regular pricing
|
||||||
if (isWholesaleAvailable) {
|
if (isWholesaleAvailable) {
|
||||||
binding.containerWholesale.visibility = View.VISIBLE
|
binding.containerWholesale.visibility = View.VISIBLE
|
||||||
binding.tvPriceWholesale.text = "Rp${formatCurrency(product.wholesalePrice!!.toDouble())}"
|
binding.tvPriceWholesale.text = formatCurrency(product.wholesalePrice!!.toDouble())
|
||||||
binding.descMinOrder.text = "Minimal pembelian ${minOrder}"
|
binding.descMinOrder.text = "Minimal pembelian ${minOrder}"
|
||||||
} else {
|
} else {
|
||||||
binding.containerWholesale.visibility = View.GONE
|
binding.containerWholesale.visibility = View.GONE
|
||||||
@ -281,6 +281,17 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
.load(fullImageUrl)
|
.load(fullImageUrl)
|
||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(binding.ivProductImage)
|
.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) {
|
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) {
|
private fun showQuantityDialog(productId: Int, isBuyNow: Boolean) {
|
||||||
val bottomSheetDialog = BottomSheetDialog(this)
|
val bottomSheetDialog = BottomSheetDialog(this)
|
||||||
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
|
val view = layoutInflater.inflate(R.layout.dialog_count_buy, null)
|
||||||
@ -377,10 +389,9 @@ class DetailProductActivity : AppCompatActivity() {
|
|||||||
switchWholesale.visibility = View.VISIBLE
|
switchWholesale.visibility = View.VISIBLE
|
||||||
Toast.makeText(this, "Minimal pembelian grosir $currentQuantity produk", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Minimal pembelian grosir $currentQuantity produk", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
|
titleWholesale.visibility = View.GONE
|
||||||
switchWholesale.visibility = View.GONE
|
switchWholesale.visibility = View.GONE
|
||||||
}
|
}
|
||||||
// Set initial quantity based on current selection
|
|
||||||
|
|
||||||
|
|
||||||
switchWholesale.setOnCheckedChangeListener { _, isChecked ->
|
switchWholesale.setOnCheckedChangeListener { _, isChecked ->
|
||||||
isWholesaleSelected = isChecked
|
isWholesaleSelected = isChecked
|
||||||
|
@ -2,6 +2,7 @@ package com.alya.ecommerce_serang.ui.product
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -35,7 +36,16 @@ class OtherProductAdapter (
|
|||||||
|
|
||||||
tvProductName.text = product.name
|
tvProductName.text = product.name
|
||||||
tvProductPrice.text = formatCurrency(product.price.toDouble())
|
tvProductPrice.text = formatCurrency(product.price.toDouble())
|
||||||
rating.text = product.rating
|
val ratingStr = product.rating
|
||||||
|
val ratingValue = ratingStr?.toFloatOrNull()
|
||||||
|
|
||||||
|
if (ratingValue != null && ratingValue > 0f) {
|
||||||
|
binding.rating.text = String.format("%.1f", ratingValue)
|
||||||
|
binding.rating.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.rating.text = "Belum ada rating"
|
||||||
|
binding.rating.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
// Load image using Glide
|
// Load image using Glide
|
||||||
Glide.with(itemView)
|
Glide.with(itemView)
|
||||||
|
@ -128,7 +128,6 @@ class StoreDetailActivity : AppCompatActivity() {
|
|||||||
private fun updateStoreInfo(store: StoreItem?) {
|
private fun updateStoreInfo(store: StoreItem?) {
|
||||||
store?.let {
|
store?.let {
|
||||||
binding.tvStoreName.text = it.storeName
|
binding.tvStoreName.text = it.storeName
|
||||||
binding.tvStoreRating.text = it.storeRating
|
|
||||||
binding.tvStoreLocation.text = it.storeLocation
|
binding.tvStoreLocation.text = it.storeLocation
|
||||||
binding.tvStoreType.text = it.storeType
|
binding.tvStoreType.text = it.storeType
|
||||||
binding.tvActiveStatus.text = it.status
|
binding.tvActiveStatus.text = it.status
|
||||||
@ -145,6 +144,17 @@ class StoreDetailActivity : AppCompatActivity() {
|
|||||||
.load(fullImageUrl)
|
.load(fullImageUrl)
|
||||||
.placeholder(R.drawable.placeholder_image)
|
.placeholder(R.drawable.placeholder_image)
|
||||||
.into(binding.ivStoreImage)
|
.into(binding.ivStoreImage)
|
||||||
|
|
||||||
|
val ratingStr = it.storeRating
|
||||||
|
val ratingValue = ratingStr?.toFloatOrNull()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,24 +7,26 @@ import android.widget.Toast
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
import com.alya.ecommerce_serang.data.api.dto.Store
|
import com.alya.ecommerce_serang.data.api.dto.Store
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
||||||
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityMyStoreBinding
|
import com.alya.ecommerce_serang.databinding.ActivityMyStoreBinding
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.balance.BalanceActivity
|
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.chat.ChatListStoreActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.product.ProductActivity
|
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.profile.DetailStoreProfileActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.review.ReviewFragment
|
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.sells.SellsActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.MyStoreViewModel
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MyStoreActivity : AppCompatActivity() {
|
class MyStoreActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMyStoreBinding
|
private lateinit var binding: ActivityMyStoreBinding
|
||||||
@ -49,14 +51,16 @@ class MyStoreActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
binding.header.headerTitle.text = "Toko Saya"
|
|
||||||
|
|
||||||
binding.header.headerLeftIcon.setOnClickListener {
|
binding.headerMyStore.headerTitle.text = "Toko Saya"
|
||||||
|
|
||||||
|
binding.headerMyStore.headerLeftIcon.setOnClickListener {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadMyStore()
|
viewModel.loadMyStore()
|
||||||
|
viewModel.loadMyStoreProducts()
|
||||||
|
|
||||||
viewModel.myStoreProfile.observe(this){ user ->
|
viewModel.myStoreProfile.observe(this){ user ->
|
||||||
user?.let { myStoreProfileOverview(it) }
|
user?.let { myStoreProfileOverview(it) }
|
||||||
@ -65,8 +69,11 @@ class MyStoreActivity : AppCompatActivity() {
|
|||||||
viewModel.errorMessage.observe(this) { error ->
|
viewModel.errorMessage.observe(this) { error ->
|
||||||
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpClickListeners()
|
setUpClickListeners()
|
||||||
|
getCountOrder()
|
||||||
|
observeViewModel()
|
||||||
|
viewModel.fetchBalance()
|
||||||
|
fetchBalance()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun myStoreProfileOverview(store: Store){
|
private fun myStoreProfileOverview(store: Store){
|
||||||
@ -140,15 +147,67 @@ class MyStoreActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCountOrder(){
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val allCounts = viewModel.getAllStatusCounts()
|
||||||
|
val totalUnpaid = allCounts["unpaid"]
|
||||||
|
val totalPaid = allCounts["paid"]
|
||||||
|
val totalProcessed = allCounts["processed"]
|
||||||
|
Log.d("MyStoreActivity",
|
||||||
|
"Total orders: unpaid=$totalUnpaid, processed=$totalProcessed, paid=$totalPaid")
|
||||||
|
|
||||||
|
binding.tvNumPesananMasuk.text = totalUnpaid.toString()
|
||||||
|
binding.tvNumPembayaran.text = totalPaid.toString()
|
||||||
|
binding.tvNumPerluDikirim.text = totalProcessed.toString()
|
||||||
|
} catch (e:Exception){
|
||||||
|
Log.e("MyStoreActivity", "Error getting order counts: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchBalance(){
|
||||||
|
viewModel.balanceResult.observe(this){result ->
|
||||||
|
when (result) {
|
||||||
|
is com.alya.ecommerce_serang.data.repository.Result.Loading ->
|
||||||
|
null
|
||||||
|
is com.alya.ecommerce_serang.data.repository.Result.Success ->
|
||||||
|
viewModel.formattedBalance.observe(this) {
|
||||||
|
binding.tvBalance.text = it
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e(
|
||||||
|
"MyStoreActivity",
|
||||||
|
"Gagal memuat saldo: ${result.exception.localizedMessage}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
companion object {
|
||||||
private const val PROFILE_REQUEST_CODE = 100
|
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()
|
|
||||||
// }
|
|
||||||
}
|
}
|
@ -22,6 +22,7 @@ import androidx.activity.viewModels
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@ -36,8 +37,6 @@ import com.alya.ecommerce_serang.ui.order.address.ProvinceAdapter
|
|||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.RegisterStoreViewModel
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
|
|
||||||
class RegisterStoreActivity : AppCompatActivity() {
|
class RegisterStoreActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -157,7 +156,7 @@ class RegisterStoreActivity : AppCompatActivity() {
|
|||||||
!viewModel.bankName.value.isNullOrBlank() &&
|
!viewModel.bankName.value.isNullOrBlank() &&
|
||||||
(viewModel.bankNumber.value ?: 0) > 0 &&
|
(viewModel.bankNumber.value ?: 0) > 0 &&
|
||||||
(viewModel.provinceId.value ?: 0) > 0 &&
|
(viewModel.provinceId.value ?: 0) > 0 &&
|
||||||
(viewModel.cityId.value ?: 0) > 0 &&
|
!viewModel.cityId.value.isNullOrBlank() &&
|
||||||
(viewModel.storeTypeId.value ?: 0) > 0 &&
|
(viewModel.storeTypeId.value ?: 0) > 0 &&
|
||||||
viewModel.ktpUri != null &&
|
viewModel.ktpUri != null &&
|
||||||
viewModel.nibUri != null &&
|
viewModel.nibUri != null &&
|
||||||
|
@ -408,6 +408,10 @@ class BalanceActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateTotalBalance(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TOP_UP_REQUEST_CODE = 101
|
private const val TOP_UP_REQUEST_CODE = 101
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
import androidx.core.view.WindowInsetsAnimationCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||||
import com.alya.ecommerce_serang.R
|
import com.alya.ecommerce_serang.R
|
||||||
@ -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}")
|
Log.d(TAG, "State updated - Messages: ${state.messages.size}")
|
||||||
|
|
||||||
// Update messages
|
// Update messages
|
||||||
@ -426,15 +428,16 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show typing indicator
|
// Show typing indicator
|
||||||
binding.tvTypingIndicator.visibility =
|
// binding.tvTypingIndicator.visibility =
|
||||||
if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
// if (state.isOtherUserTyping) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Show error if any
|
// Show error if any
|
||||||
state.error?.let { error ->
|
state.error?.let { error ->
|
||||||
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@ChatStoreActivity, error, Toast.LENGTH_SHORT).show()
|
||||||
viewModel.clearError()
|
viewModel.clearError()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showOptionsMenu() {
|
private fun showOptionsMenu() {
|
||||||
@ -520,6 +523,19 @@ class ChatStoreActivity : AppCompatActivity() {
|
|||||||
private fun handleSelectedImage(uri: Uri) {
|
private fun handleSelectedImage(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Processing selected image: $uri")
|
Log.d(TAG, "Processing selected image: $uri")
|
||||||
|
binding.layoutAttachImage.visibility = View.VISIBLE
|
||||||
|
val fullImageUrl = when (val img = uri.toString()) {
|
||||||
|
is String -> {
|
||||||
|
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||||
|
}
|
||||||
|
else -> R.drawable.placeholder_image
|
||||||
|
}
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(fullImageUrl)
|
||||||
|
.placeholder(R.drawable.placeholder_image)
|
||||||
|
.into(binding.ivAttach)
|
||||||
|
Log.d(TAG, "Display attach image: $uri")
|
||||||
|
|
||||||
// Always use the copy-to-cache approach for reliability
|
// Always use the copy-to-cache approach for reliability
|
||||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||||
|
@ -12,30 +12,29 @@ import android.view.View
|
|||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import com.alya.ecommerce_serang.R
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.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.CategoryItem
|
||||||
import com.alya.ecommerce_serang.data.api.dto.Preorder
|
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.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
|
import com.alya.ecommerce_serang.databinding.ActivityDetailStoreProductBinding
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
|
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
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() {
|
class DetailStoreProductActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
|
|||||||
val isEditing = intent.getBooleanExtra("is_editing", false)
|
val isEditing = intent.getBooleanExtra("is_editing", false)
|
||||||
productId = intent.getIntExtra("product_id", -1)
|
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) {
|
if (isEditing && productId != null && productId != -1) {
|
||||||
viewModel.loadProductDetail(productId!!)
|
viewModel.loadProductDetail(productId!!)
|
||||||
@ -140,7 +139,7 @@ class DetailStoreProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.header.headerLeftIcon.setOnClickListener {
|
binding.headerStoreProduct.headerLeftIcon.setOnClickListener {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
|||||||
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
import com.alya.ecommerce_serang.data.repository.ProductRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
import com.alya.ecommerce_serang.databinding.ActivityProductBinding
|
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.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
|
import com.alya.ecommerce_serang.utils.viewmodel.ProductViewModel
|
||||||
|
|
||||||
class ProductActivity : AppCompatActivity() {
|
class ProductActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -94,14 +94,14 @@ class ProductActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupHeader() {
|
private fun setupHeader() {
|
||||||
binding.header.headerTitle.text = "Produk Saya"
|
binding.headerListProduct.headerTitle.text = "Produk Saya"
|
||||||
binding.header.headerRightText.visibility = View.VISIBLE
|
binding.headerListProduct.headerRightText.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.header.headerLeftIcon.setOnClickListener {
|
binding.headerListProduct.headerLeftIcon.setOnClickListener {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.header.headerRightText.setOnClickListener {
|
binding.headerListProduct.headerRightText.setOnClickListener {
|
||||||
val intent = Intent(this, DetailStoreProductActivity::class.java)
|
val intent = Intent(this, DetailStoreProductActivity::class.java)
|
||||||
intent.putExtra("is_editing", false)
|
intent.putExtra("is_editing", false)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@ -111,4 +111,6 @@ class ProductActivity : AppCompatActivity() {
|
|||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
binding.rvStoreProduct.layoutManager = LinearLayoutManager(this)
|
binding.rvStoreProduct.layoutManager = LinearLayoutManager(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.sells.OrdersItem
|
import com.alya.ecommerce_serang.data.api.response.store.sells.OrdersItem
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
@ -16,12 +17,14 @@ import com.alya.ecommerce_serang.data.repository.Result
|
|||||||
import com.alya.ecommerce_serang.data.repository.SellsRepository
|
import com.alya.ecommerce_serang.data.repository.SellsRepository
|
||||||
import com.alya.ecommerce_serang.databinding.FragmentSellsListBinding
|
import com.alya.ecommerce_serang.databinding.FragmentSellsListBinding
|
||||||
import com.alya.ecommerce_serang.ui.order.address.ViewState
|
import com.alya.ecommerce_serang.ui.order.address.ViewState
|
||||||
|
import com.alya.ecommerce_serang.ui.profile.mystore.MyStoreActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.sells.payment.DetailPaymentActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.sells.payment.DetailPaymentActivity
|
||||||
import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmentActivity
|
import com.alya.ecommerce_serang.ui.profile.mystore.sells.shipment.DetailShipmentActivity
|
||||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||||
import com.alya.ecommerce_serang.utils.SessionManager
|
import com.alya.ecommerce_serang.utils.SessionManager
|
||||||
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
|
import com.alya.ecommerce_serang.utils.viewmodel.SellsViewModel
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SellsListFragment : Fragment() {
|
class SellsListFragment : Fragment() {
|
||||||
|
|
||||||
@ -84,6 +87,7 @@ class SellsListFragment : Fragment() {
|
|||||||
observeSellsList()
|
observeSellsList()
|
||||||
observePaymentConfirmation()
|
observePaymentConfirmation()
|
||||||
loadSells()
|
loadSells()
|
||||||
|
// getAllOrderCountsAndNavigate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
@ -183,6 +187,30 @@ class SellsListFragment : Fragment() {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAllOrderCountsAndNavigate() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
// Show loading if needed
|
||||||
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val allCounts = viewModel.getAllStatusCounts()
|
||||||
|
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
|
||||||
|
val intent = Intent(requireContext(), MyStoreActivity::class.java)
|
||||||
|
intent.putExtra("total_unpaid", allCounts["unpaid"])
|
||||||
|
intent.putExtra("total_paid", allCounts["paid"])
|
||||||
|
intent.putExtra("total_processed", allCounts["processed"])
|
||||||
|
Log.d("SellsListFragment", "Total orders: unpaid=${allCounts["unpaid"]}, processed=${allCounts["processed"]}, Paid=${allCounts["paid"]}")
|
||||||
|
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
Log.e(TAG, "Error getting order counts: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
package com.alya.ecommerce_serang.utils.viewmodel
|
package com.alya.ecommerce_serang.utils.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.viewModelScope
|
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.dto.Store
|
||||||
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
|
import com.alya.ecommerce_serang.data.api.response.auth.StoreTypesItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.store.StoreResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
import com.alya.ecommerce_serang.data.api.response.store.profile.StoreDataResponse
|
||||||
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
import com.alya.ecommerce_serang.data.repository.MyStoreRepository
|
||||||
import com.alya.ecommerce_serang.data.repository.Result
|
import com.alya.ecommerce_serang.data.repository.Result
|
||||||
@ -13,6 +17,8 @@ import kotlinx.coroutines.launch
|
|||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
|
class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
|
||||||
private val _myStoreProfile = MutableLiveData<Store?>()
|
private val _myStoreProfile = MutableLiveData<Store?>()
|
||||||
@ -30,6 +36,12 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
|
|||||||
private val _errorMessage = MutableLiveData<String>()
|
private val _errorMessage = MutableLiveData<String>()
|
||||||
val errorMessage : LiveData<String> = _errorMessage
|
val errorMessage : LiveData<String> = _errorMessage
|
||||||
|
|
||||||
|
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(){
|
fun loadMyStore(){
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = repository.fetchMyStoreProfile()){
|
when (val result = repository.fetchMyStoreProfile()){
|
||||||
@ -100,6 +112,68 @@ class MyStoreViewModel(private val repository: MyStoreRepository): ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getTotalOrdersByStatus(status: String): Int {
|
||||||
|
return try {
|
||||||
|
when (val result = repository.getSellList(status)) {
|
||||||
|
is Result.Success -> {
|
||||||
|
// Access the orders list from the response
|
||||||
|
result.data.orders.size ?: 0
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e("SellsViewModel", "Error getting orders count: ${result.exception.message}")
|
||||||
|
0
|
||||||
|
}
|
||||||
|
is Result.Loading -> 0
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SellsViewModel", "Exception getting orders count", e)
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//count the order
|
||||||
|
suspend fun getAllStatusCounts(): Map<String, Int> {
|
||||||
|
val statuses = listOf( "unpaid", "paid", "processed")
|
||||||
|
val counts = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
statuses.forEach { status ->
|
||||||
|
counts[status] = getTotalOrdersByStatus(status)
|
||||||
|
Log.d("SellsViewModel", "Status: $status, countOrder=${counts[status]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
|
val formattedBalance: LiveData<String> = balanceResult.map { result ->
|
||||||
|
when (result) {
|
||||||
|
is Result.Success -> {
|
||||||
|
val raw = result.data.store.balance.toDouble()
|
||||||
|
NumberFormat.getCurrencyInstance(Locale("in", "ID")).format(raw)
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trigger the network call */
|
||||||
|
fun fetchBalance() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_balanceResult.value = Result.Loading
|
||||||
|
_balanceResult.value = repository.getBalance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =
|
private fun String.toRequestBody(): RequestBody =
|
||||||
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
|
RequestBody.create("text/plain".toMediaTypeOrNull(), this)
|
||||||
}
|
}
|
@ -42,7 +42,7 @@ class RegisterStoreViewModel(
|
|||||||
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
|
val citiesState: LiveData<Result<List<CitiesItem>>> = _citiesState
|
||||||
|
|
||||||
var selectedProvinceId: Int? = null
|
var selectedProvinceId: Int? = null
|
||||||
var selectedCityId: Int? = null
|
var selectedCityId: String? = null
|
||||||
|
|
||||||
// Form fields
|
// Form fields
|
||||||
val storeName = MutableLiveData<String>()
|
val storeName = MutableLiveData<String>()
|
||||||
@ -52,7 +52,7 @@ class RegisterStoreViewModel(
|
|||||||
val longitude = MutableLiveData<String>()
|
val longitude = MutableLiveData<String>()
|
||||||
val street = MutableLiveData<String>()
|
val street = MutableLiveData<String>()
|
||||||
val subdistrict = MutableLiveData<String>()
|
val subdistrict = MutableLiveData<String>()
|
||||||
val cityId = MutableLiveData<Int>()
|
val cityId = MutableLiveData<String>()
|
||||||
val provinceId = MutableLiveData<Int>()
|
val provinceId = MutableLiveData<Int>()
|
||||||
val postalCode = MutableLiveData<Int>()
|
val postalCode = MutableLiveData<Int>()
|
||||||
val addressDetail = MutableLiveData<String>()
|
val addressDetail = MutableLiveData<String>()
|
||||||
@ -122,7 +122,7 @@ class RegisterStoreViewModel(
|
|||||||
longitude = longitude.value ?: "",
|
longitude = longitude.value ?: "",
|
||||||
street = street.value ?: "",
|
street = street.value ?: "",
|
||||||
subdistrict = subdistrict.value ?: "",
|
subdistrict = subdistrict.value ?: "",
|
||||||
cityId = cityId.value ?: 0,
|
cityId = cityId.value ?: "",
|
||||||
provinceId = provinceId.value ?: 0,
|
provinceId = provinceId.value ?: 0,
|
||||||
postalCode = postalCode.value ?: 0,
|
postalCode = postalCode.value ?: 0,
|
||||||
detail = addressDetail.value ?: "",
|
detail = addressDetail.value ?: "",
|
||||||
|
@ -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.auth.VerifRegisterResponse
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
|
import com.alya.ecommerce_serang.data.api.response.customer.order.CitiesItem
|
||||||
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
|
import com.alya.ecommerce_serang.data.api.response.customer.order.ProvincesItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.SubdistrictsItem
|
||||||
|
import com.alya.ecommerce_serang.data.api.response.customer.order.VillagesItem
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||||
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
import com.alya.ecommerce_serang.data.repository.OrderRepository
|
||||||
@ -62,17 +64,29 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
|
|||||||
private val _registeredUser = MutableLiveData<User>()
|
private val _registeredUser = MutableLiveData<User>()
|
||||||
val registeredUser: LiveData<User> = _registeredUser
|
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
|
// For address data
|
||||||
var selectedProvinceId: Int? = null
|
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>>>()
|
private val _provincesState = MutableLiveData<ViewState<List<ProvincesItem>>>()
|
||||||
val provincesState: LiveData<ViewState<List<ProvincesItem>>> = _provincesState
|
val provincesState: LiveData<ViewState<List<ProvincesItem>>> = _provincesState
|
||||||
|
|
||||||
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
|
private val _citiesState = MutableLiveData<ViewState<List<CitiesItem>>>()
|
||||||
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
|
val citiesState: LiveData<ViewState<List<CitiesItem>>> = _citiesState
|
||||||
|
|
||||||
|
private val _subdistrictState = MutableLiveData<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
|
// For address submission
|
||||||
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
|
private val _addressSubmissionState = MutableLiveData<ViewState<String>>()
|
||||||
val addressSubmissionState: LiveData<ViewState<String>> = _addressSubmissionState
|
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}")
|
Log.d("RegisterViewModel", "OTP Response: ${response.available}")
|
||||||
_checkValue.value = Result.Success(response.available)// Store the message for UI feedback
|
_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) {
|
} catch (exception: Exception) {
|
||||||
// Handle any errors and update state
|
// Handle any errors and update state
|
||||||
_checkValue.value = Result.Error(exception)
|
_checkValue.value = Result.Error(exception)
|
||||||
|
_toastMessage.value = Event("Gagal memeriksa ${request.fieldRegis}")
|
||||||
|
|
||||||
// Log the error for debugging
|
// Log the error for debugging
|
||||||
Log.e("RegisterViewModel", "Error:", exception)
|
Log.e("RegisterViewModel", "Error:", exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//using raja ongkir
|
||||||
fun getProvinces() {
|
fun getProvinces() {
|
||||||
_provincesState.value = ViewState.Loading
|
_provincesState.value = ViewState.Loading
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -242,6 +263,7 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//kota pake raja ongkir
|
||||||
fun getCities(provinceId: Int) {
|
fun getCities(provinceId: Int) {
|
||||||
_citiesState.value = ViewState.Loading
|
_citiesState.value = ViewState.Loading
|
||||||
viewModelScope.launch {
|
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) {
|
fun setSelectedProvinceId(id: Int) {
|
||||||
selectedProvinceId = id
|
selectedProvinceId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelectedCityId(id: Int) {
|
fun updateSelectedCityId(id: String) {
|
||||||
selectedCityId = id
|
selectedCityId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSelectedSubdistrict(id: String){
|
||||||
|
selectedSubdistrict = id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSelectedVillages(id: String){
|
||||||
|
selectedVillages = id
|
||||||
|
}
|
||||||
|
|
||||||
fun addAddress(request: CreateAddressRequest) {
|
fun addAddress(request: CreateAddressRequest) {
|
||||||
Log.d(TAG, "Starting address submission process")
|
Log.d(TAG, "Starting address submission process")
|
||||||
_addressSubmissionState.value = ViewState.Loading
|
_addressSubmissionState.value = ViewState.Loading
|
||||||
@ -314,5 +386,9 @@ class RegisterViewModel(private val repository: UserRepository, private val orde
|
|||||||
private const val TAG = "RegisterViewModel"
|
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 }
|
||||||
}
|
}
|
@ -200,6 +200,38 @@ class SellsViewModel(private val repository: SellsRepository) : ViewModel() {
|
|||||||
|
|
||||||
Log.d(TAG, "========== getSellList method completed ==========")
|
Log.d(TAG, "========== getSellList method completed ==========")
|
||||||
}
|
}
|
||||||
|
//get total order each status
|
||||||
|
suspend fun getTotalOrdersByStatus(status: String): Int {
|
||||||
|
return try {
|
||||||
|
when (val result = repository.getSellList(status)) {
|
||||||
|
is Result.Success -> {
|
||||||
|
// Access the orders list from the response
|
||||||
|
result.data.orders.size ?: 0
|
||||||
|
}
|
||||||
|
is Result.Error -> {
|
||||||
|
Log.e("SellsViewModel", "Error getting orders count: ${result.exception.message}")
|
||||||
|
0
|
||||||
|
}
|
||||||
|
is Result.Loading -> 0
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SellsViewModel", "Exception getting orders count", e)
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//count the order
|
||||||
|
suspend fun getAllStatusCounts(): Map<String, Int> {
|
||||||
|
val statuses = listOf( "unpaid", "paid", "processed")
|
||||||
|
val counts = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
statuses.forEach { status ->
|
||||||
|
counts[status] = getTotalOrdersByStatus(status)
|
||||||
|
Log.d("SellsViewModel", "Status: $status, countOrder=${counts[status]}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts
|
||||||
|
}
|
||||||
|
|
||||||
fun getSellDetails(orderId: Int) {
|
fun getSellDetails(orderId: Int) {
|
||||||
Log.d(TAG, "========== Starting getSellDetails ==========")
|
Log.d(TAG, "========== Starting getSellDetails ==========")
|
||||||
|
9
app/src/main/res/drawable/ic_close_chat.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="14dp"
|
||||||
|
android:viewportWidth="14"
|
||||||
|
android:viewportHeight="14">
|
||||||
|
<path
|
||||||
|
android:pathData="M7,8.4L2.1,13.3C1.917,13.483 1.683,13.575 1.4,13.575C1.117,13.575 0.883,13.483 0.7,13.3C0.517,13.116 0.425,12.883 0.425,12.6C0.425,12.316 0.517,12.083 0.7,11.9L5.6,7L0.7,2.1C0.517,1.916 0.425,1.683 0.425,1.4C0.425,1.116 0.517,0.883 0.7,0.7C0.883,0.516 1.117,0.425 1.4,0.425C1.683,0.425 1.917,0.516 2.1,0.7L7,5.6L11.9,0.7C12.083,0.516 12.317,0.425 12.6,0.425C12.883,0.425 13.117,0.516 13.3,0.7C13.483,0.883 13.575,1.116 13.575,1.4C13.575,1.683 13.483,1.916 13.3,2.1L8.4,7L13.3,11.9C13.483,12.083 13.575,12.316 13.575,12.6C13.575,12.883 13.483,13.116 13.3,13.3C13.117,13.483 12.883,13.575 12.6,13.575C12.317,13.575 12.083,13.483 11.9,13.3L7,8.4Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
@ -1,170 +1,74 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
|
android:width="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<path
|
<path android:fillColor="#3DDC84"
|
||||||
android:fillColor="#3DDC84"
|
android:pathData="M0,0h108v108h-108z"/>
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||||
android:pathData="M9,0L9,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M19,0L19,108"
|
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||||
android:pathData="M29,0L29,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M39,0L39,108"
|
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||||
android:pathData="M49,0L49,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M59,0L59,108"
|
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||||
android:pathData="M69,0L69,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M79,0L79,108"
|
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||||
android:pathData="M89,0L89,108"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M99,0L99,108"
|
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||||
android:pathData="M0,9L108,9"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||||
android:strokeColor="#33FFFFFF" />
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
<path
|
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||||
android:fillColor="#00000000"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:pathData="M0,19L108,19"
|
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||||
android:strokeWidth="0.8"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeColor="#33FFFFFF" />
|
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||||
<path
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:fillColor="#00000000"
|
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||||
android:pathData="M0,29L108,29"
|
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -1,30 +1,20 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="64"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="64">
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
<group android:scaleX="0.6722222"
|
||||||
<aapt:attr name="android:fillColor">
|
android:scaleY="0.6722222"
|
||||||
<gradient
|
android:translateX="10.488889"
|
||||||
android:endX="85.84757"
|
android:translateY="10.488889">
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:pathData="M0,0h64v64h-64z"
|
||||||
android:fillType="nonZero"
|
android:fillColor="#489EC6"/>
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
<path
|
||||||
android:strokeWidth="1"
|
android:pathData="M11.868,58C10.523,58 9.399,57.538 8.497,56.614C7.594,55.69 7.144,54.537 7.146,53.155V29.074C5.912,28.14 5.009,26.915 4.438,25.399C3.867,23.883 3.854,22.266 4.4,20.548L7.245,10.924C7.635,9.7 8.264,8.74 9.131,8.044C10.001,7.348 11.075,7 12.354,7H51.536C52.813,7 53.883,7.326 54.747,7.978C55.608,8.63 56.24,9.573 56.641,10.807L59.598,20.545C60.146,22.265 60.134,23.896 59.563,25.438C58.992,26.98 58.089,28.23 56.855,29.188V53.152C56.855,54.534 56.404,55.687 55.501,56.611C54.599,57.535 53.476,57.998 52.133,58H11.868ZM38.433,28C40.311,28 41.712,27.46 42.638,26.38C43.564,25.302 43.954,24.188 43.808,23.038L41.866,10H33.465V22.6C33.465,24.074 33.957,25.342 34.939,26.404C35.922,27.468 37.084,28 38.433,28ZM25.278,28C26.849,28 28.119,27.468 29.088,26.404C30.057,25.34 30.541,24.072 30.541,22.6V10H22.138L20.19,23.269C20.071,24.201 20.468,25.222 21.38,26.332C22.292,27.442 23.594,27.998 25.278,28ZM12.263,28C13.551,28 14.647,27.55 15.55,26.65C16.452,25.75 17.014,24.627 17.234,23.281L19.067,10H12.354C11.716,10 11.209,10.144 10.833,10.432C10.457,10.72 10.176,11.153 9.991,11.731L7.292,21.205C6.812,22.781 7.017,24.308 7.906,25.786C8.795,27.264 10.247,28.002 12.263,28ZM51.738,28C53.488,28 54.886,27.3 55.931,25.9C56.978,24.5 57.237,22.935 56.709,21.205L53.864,11.617C53.676,11.039 53.395,10.625 53.019,10.375C52.642,10.125 52.137,10 51.501,10H44.933L46.767,23.281C46.987,24.627 47.549,25.75 48.451,26.65C49.354,27.55 50.449,28 51.738,28Z"
|
||||||
android:strokeColor="#00000000" />
|
android:fillColor="#ffffff"/>
|
||||||
</vector>
|
<path
|
||||||
|
android:pathData="M20.382,39V30.6H23.97C24.554,30.6 25.046,30.692 25.446,30.876C25.846,31.052 26.15,31.304 26.358,31.632C26.574,31.952 26.682,32.332 26.682,32.772C26.682,33.196 26.59,33.552 26.406,33.84C26.222,34.128 25.978,34.348 25.674,34.5C25.378,34.652 25.05,34.744 24.69,34.776L24.882,34.632C25.274,34.648 25.618,34.752 25.914,34.944C26.21,35.136 26.442,35.388 26.61,35.7C26.786,36.004 26.874,36.34 26.874,36.708C26.874,37.164 26.766,37.564 26.55,37.908C26.334,38.252 26.018,38.52 25.602,38.712C25.186,38.904 24.682,39 24.09,39H20.382ZM22.182,37.536H23.79C24.19,37.536 24.498,37.448 24.714,37.272C24.938,37.088 25.05,36.824 25.05,36.48C25.05,36.136 24.934,35.868 24.702,35.676C24.478,35.484 24.166,35.388 23.766,35.388H22.182V37.536ZM22.182,34.056H23.658C24.042,34.056 24.334,33.968 24.534,33.792C24.742,33.616 24.846,33.368 24.846,33.048C24.846,32.728 24.742,32.48 24.534,32.304C24.334,32.12 24.038,32.028 23.646,32.028H22.182V34.056ZM28.163,39V32.952H29.963V39H28.163ZM29.063,32.256C28.743,32.256 28.483,32.164 28.283,31.98C28.083,31.796 27.983,31.564 27.983,31.284C27.983,30.996 28.083,30.76 28.283,30.576C28.483,30.392 28.743,30.3 29.063,30.3C29.391,30.3 29.655,30.392 29.855,30.576C30.063,30.76 30.167,30.996 30.167,31.284C30.167,31.564 30.063,31.796 29.855,31.98C29.655,32.164 29.391,32.256 29.063,32.256ZM34.069,39.144C33.501,39.144 33.009,39.056 32.593,38.88C32.186,38.696 31.861,38.448 31.622,38.136C31.389,37.824 31.257,37.472 31.226,37.08H33.014C33.046,37.216 33.102,37.34 33.181,37.452C33.27,37.556 33.386,37.64 33.529,37.704C33.681,37.76 33.849,37.788 34.034,37.788C34.234,37.788 34.394,37.764 34.514,37.716C34.641,37.66 34.737,37.588 34.801,37.5C34.866,37.412 34.897,37.32 34.897,37.224C34.897,37.072 34.849,36.956 34.754,36.876C34.666,36.796 34.534,36.732 34.357,36.684C34.181,36.628 33.97,36.576 33.722,36.528C33.433,36.464 33.146,36.392 32.857,36.312C32.577,36.224 32.326,36.116 32.102,35.988C31.885,35.86 31.709,35.696 31.573,35.496C31.445,35.288 31.382,35.036 31.382,34.74C31.382,34.38 31.482,34.056 31.681,33.768C31.882,33.472 32.169,33.24 32.546,33.072C32.922,32.896 33.377,32.808 33.914,32.808C34.674,32.808 35.27,32.976 35.701,33.312C36.133,33.648 36.389,34.1 36.47,34.668H34.79C34.742,34.508 34.641,34.388 34.489,34.308C34.338,34.22 34.146,34.176 33.914,34.176C33.65,34.176 33.45,34.22 33.313,34.308C33.178,34.396 33.11,34.512 33.11,34.656C33.11,34.752 33.153,34.84 33.242,34.92C33.338,34.992 33.473,35.056 33.65,35.112C33.826,35.168 34.042,35.224 34.298,35.28C34.785,35.384 35.206,35.496 35.557,35.616C35.917,35.736 36.197,35.912 36.397,36.144C36.597,36.368 36.694,36.696 36.686,37.128C36.694,37.52 36.59,37.868 36.374,38.172C36.166,38.476 35.866,38.716 35.473,38.892C35.082,39.06 34.613,39.144 34.069,39.144ZM40.094,39.144C39.59,39.144 39.17,39.064 38.834,38.904C38.506,38.744 38.262,38.528 38.102,38.256C37.95,37.976 37.874,37.668 37.874,37.332C37.874,36.972 37.962,36.656 38.138,36.384C38.322,36.104 38.606,35.884 38.99,35.724C39.374,35.556 39.858,35.472 40.442,35.472H41.906C41.906,35.2 41.87,34.976 41.798,34.8C41.734,34.624 41.626,34.492 41.474,34.404C41.322,34.316 41.114,34.272 40.85,34.272C40.57,34.272 40.334,34.328 40.142,34.44C39.95,34.552 39.83,34.728 39.782,34.968H38.054C38.094,34.536 38.234,34.16 38.474,33.84C38.722,33.52 39.05,33.268 39.458,33.084C39.866,32.9 40.334,32.808 40.862,32.808C41.438,32.808 41.938,32.904 42.362,33.096C42.786,33.28 43.114,33.552 43.346,33.912C43.586,34.272 43.706,34.72 43.706,35.256V39H42.206L41.99,38.124C41.902,38.276 41.798,38.416 41.678,38.544C41.558,38.664 41.418,38.772 41.258,38.868C41.098,38.956 40.922,39.024 40.73,39.072C40.538,39.12 40.326,39.144 40.094,39.144ZM40.538,37.776C40.73,37.776 40.898,37.744 41.042,37.68C41.186,37.616 41.31,37.528 41.414,37.416C41.518,37.304 41.602,37.176 41.666,37.032C41.738,36.88 41.79,36.716 41.822,36.54V36.528H40.658C40.458,36.528 40.29,36.556 40.154,36.612C40.026,36.66 39.93,36.732 39.866,36.828C39.802,36.924 39.77,37.036 39.77,37.164C39.77,37.3 39.802,37.416 39.866,37.512C39.938,37.6 40.03,37.668 40.142,37.716C40.262,37.756 40.394,37.776 40.538,37.776ZM17.132,53.144C16.5,53.144 15.924,53.02 15.404,52.772C14.892,52.516 14.484,52.136 14.18,51.632C13.884,51.128 13.736,50.492 13.736,49.724V44.6H15.536V49.736C15.536,50.112 15.596,50.432 15.716,50.696C15.844,50.96 16.028,51.16 16.268,51.296C16.516,51.424 16.812,51.488 17.156,51.488C17.508,51.488 17.804,51.424 18.044,51.296C18.292,51.16 18.48,50.96 18.608,50.696C18.736,50.432 18.8,50.112 18.8,49.736V44.6H20.6V49.724C20.6,50.492 20.44,51.128 20.12,51.632C19.808,52.136 19.388,52.516 18.86,52.772C18.34,53.02 17.764,53.144 17.132,53.144ZM22.128,53V44.6H24.288L26.736,49.652L29.16,44.6H31.308V53H29.508V47.636L27.444,51.824H26.004L23.928,47.636V53H22.128ZM32.921,53V44.6H34.721V47.78L37.625,44.6H39.833L36.749,47.924L39.941,53H37.733L35.465,49.316L34.721,50.12V53H32.921ZM41.007,53V44.6H43.167L45.615,49.652L48.039,44.6H50.187V53H48.387V47.636L46.323,51.824H44.883L42.807,47.636V53H41.007Z"
|
||||||
|
android:fillColor="#489EC6"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
9
app/src/main/res/drawable/ic_sent.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="19"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,16V10L8,8L0,6V0L19,8L0,16Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</vector>
|
@ -133,6 +133,7 @@
|
|||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="Nomor Rekening / Nomor HP *"
|
android:text="Nomor Rekening / Nomor HP *"
|
||||||
android:fontFamily="@font/dmsans_semibold"
|
android:fontFamily="@font/dmsans_semibold"
|
||||||
|
android:visibility="gone"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
@ -145,6 +146,7 @@
|
|||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:minHeight="50dp"
|
android:minHeight="50dp"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
|
android:visibility="gone"
|
||||||
android:padding="12dp" />
|
android:padding="12dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -152,6 +154,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="Tanggal Pembayaran *"
|
android:text="Tanggal Pembayaran *"
|
||||||
|
android:visibility="gone"
|
||||||
android:fontFamily="@font/dmsans_semibold"
|
android:fontFamily="@font/dmsans_semibold"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
@ -164,6 +167,7 @@
|
|||||||
android:drawableEnd="@drawable/ic_calendar"
|
android:drawableEnd="@drawable/ic_calendar"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
android:hint="Pilih tanggal"
|
android:hint="Pilih tanggal"
|
||||||
|
android:visibility="gone"
|
||||||
android:minHeight="50dp"
|
android:minHeight="50dp"
|
||||||
android:padding="12dp" />
|
android:padding="12dp" />
|
||||||
|
|
||||||
|
@ -21,6 +21,18 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/header"/>
|
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
|
<TextView
|
||||||
android:id="@+id/tvWholesaleWarning"
|
android:id="@+id/tvWholesaleWarning"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -110,12 +122,14 @@
|
|||||||
android:src="@drawable/outline_shopping_cart_24" />
|
android:src="@drawable/outline_shopping_cart_24" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/emptyCart"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="Keranjang Anda kosong"
|
android:visibility="gone"
|
||||||
|
android:text="Keranjang anda kosong"
|
||||||
android:textColor="@android:color/black"
|
android:textColor="@android:color/black"
|
||||||
android:textSize="18sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -54,19 +54,9 @@
|
|||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
app:layout_constraintStart_toEndOf="@+id/imgProfile"
|
||||||
app:layout_constraintTop_toTopOf="@+id/imgProfile"
|
app:layout_constraintTop_toTopOf="@+id/imgProfile"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btnOptions" />
|
app:layout_constraintEnd_toStartOf="@+id/btnOptions"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
<!-- <TextView-->
|
|
||||||
<!-- android:id="@+id/tvLastActive"-->
|
|
||||||
<!-- android:layout_width="0dp"-->
|
|
||||||
<!-- android:layout_height="wrap_content"-->
|
|
||||||
<!-- android:layout_marginStart="8dp"-->
|
|
||||||
<!-- android:text="Aktif 3 jam lalu"-->
|
|
||||||
<!-- android:textColor="#888888"-->
|
|
||||||
<!-- android:textSize="12sp"-->
|
|
||||||
<!-- app:layout_constraintStart_toEndOf="@+id/imgProfile"-->
|
|
||||||
<!-- app:layout_constraintTop_toBottomOf="@+id/tvStoreName"-->
|
|
||||||
<!-- app:layout_constraintEnd_toEndOf="@+id/tvStoreName" />-->
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btnOptions"
|
android:id="@+id/btnOptions"
|
||||||
@ -178,22 +168,47 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="8dp"
|
android:paddingBottom="8dp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/tvTypingIndicator"
|
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/cardProduct" />
|
app:layout_constraintTop_toBottomOf="@+id/cardProduct" />
|
||||||
|
|
||||||
<!-- Typing indicator -->
|
<!-- Typing indicator -->
|
||||||
<TextView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/tvTypingIndicator"
|
android:id="@+id/layoutAttachImage"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="4dp"
|
|
||||||
android:text="User is typing..."
|
|
||||||
android:textColor="#666666"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textStyle="italic"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/layoutChatInput"
|
android:padding="4dp"
|
||||||
tools:visibility="visible" />
|
app:layout_constraintBottom_toTopOf="@id/layoutChatInput"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="4dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv_attach"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:clipToOutline="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_close_chat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:backgroundTint="@android:color/transparent"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:src="@drawable/ic_close_chat"
|
||||||
|
/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<!-- Chat input area -->
|
<!-- Chat input area -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -239,7 +254,7 @@
|
|||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="Send"
|
android:contentDescription="Send"
|
||||||
android:src="@drawable/baseline_attach_file_24" />
|
android:src="@drawable/ic_sent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -6,7 +6,7 @@
|
|||||||
android:id="@+id/main"
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/black_800"
|
android:background="@color/white"
|
||||||
android:theme="@style/Theme.Ecommerce_serang"
|
android:theme="@style/Theme.Ecommerce_serang"
|
||||||
tools:context=".ui.order.CheckoutActivity">
|
tools:context=".ui.order.CheckoutActivity">
|
||||||
|
|
||||||
@ -75,7 +75,7 @@
|
|||||||
android:id="@+id/tv_places_address"
|
android:id="@+id/tv_places_address"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Rumah"
|
android:text="-"
|
||||||
android:textColor="#5A5A5A"
|
android:textColor="#5A5A5A"
|
||||||
android:paddingHorizontal="8dp"
|
android:paddingHorizontal="8dp"
|
||||||
android:paddingVertical="2dp"
|
android:paddingVertical="2dp"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:text="Jl. Pegangasan Timur"
|
android:text="-"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:layout_marginStart="32dp" />
|
android:layout_marginStart="32dp" />
|
||||||
|
|
||||||
@ -179,9 +179,11 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/card_shipment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
app:cardCornerRadius="8dp"
|
app:cardCornerRadius="8dp"
|
||||||
app:cardElevation="0dp"
|
app:cardElevation="0dp"
|
||||||
app:cardBackgroundColor="#F5F5F5">
|
app:cardBackgroundColor="#F5F5F5">
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
tools:context=".ui.profile.mystore.product.DetailStoreProductActivity">
|
tools:context=".ui.profile.mystore.product.DetailStoreProductActivity">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/header"
|
android:id="@+id/headerStoreProduct"
|
||||||
layout="@layout/header" />
|
layout="@layout/header" />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
@ -3,38 +3,48 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
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">
|
tools:context=".ui.product.listproduct.ListProductActivity">
|
||||||
|
|
||||||
<include
|
<LinearLayout
|
||||||
android:id="@+id/searchContainerList"
|
|
||||||
layout="@layout/view_search_back"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:orientation="vertical"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@id/rvProductsList"/>
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
<include
|
||||||
|
android:id="@+id/searchContainerList"
|
||||||
|
layout="@layout/view_search_back"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"/>
|
||||||
|
|
||||||
|
<!-- <com.google.android.material.divider.MaterialDivider-->
|
||||||
|
<!-- android:id="@+id/divider_product"-->
|
||||||
|
<!-- android:layout_width="match_parent"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- android:layout_marginTop="2dp"-->
|
||||||
|
<!-- app:layout_constraintTop_toBottomOf="@id/searchContainer"/>-->
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvProductsList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="23dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchContainerList"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:spanCount="2"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:listitem="@layout/item_product_grid"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- <com.google.android.material.divider.MaterialDivider-->
|
|
||||||
<!-- android:id="@+id/divider_product"-->
|
|
||||||
<!-- android:layout_width="match_parent"-->
|
|
||||||
<!-- android:layout_height="wrap_content"-->
|
|
||||||
<!-- android:layout_marginTop="2dp"-->
|
|
||||||
<!-- app:layout_constraintTop_toBottomOf="@id/searchContainer"/>-->
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/rvProductsList"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingHorizontal="23dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/searchContainerList"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
app:spanCount="2"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
tools:listitem="@layout/item_product_grid"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,97 +1,136 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/main"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:paddingHorizontal="32dp"
|
||||||
android:layout_margin="16dp"
|
android:paddingVertical="16dp"
|
||||||
android:layout_marginHorizontal="16dp"
|
|
||||||
android:layout_marginVertical="16dp"
|
|
||||||
tools:context=".ui.auth.LoginActivity">
|
tools:context=".ui.auth.LoginActivity">
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/tv_login_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/login"
|
android:text="@string/login"
|
||||||
|
android:textAlignment="center"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textAlignment="center"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:layout_marginBottom="24dp"/>
|
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
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/tv_email_label"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:fontFamily="@font/dmsans_medium"
|
android:fontFamily="@font/dmsans_medium"
|
||||||
|
android:text="@string/login_email"
|
||||||
android:textSize="18sp"
|
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
|
<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_height="wrap_content"
|
||||||
android:layout_marginBottom="12dp"
|
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
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/et_login_email"
|
android:id="@+id/et_login_email"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/hint_login_email"
|
android:hint="@string/hint_login_email"
|
||||||
android:inputType="textEmailAddress"/>
|
android:inputType="textEmailAddress" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- Password label-->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/tv_password_label"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:fontFamily="@font/dmsans_medium"
|
android:fontFamily="@font/dmsans_medium"
|
||||||
|
android:text="@string/password"
|
||||||
android:textSize="18sp"
|
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
|
<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_height="wrap_content"
|
||||||
android:layout_marginBottom="12dp"
|
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
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/et_login_password"
|
android:id="@+id/et_login_password"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/hint_login_password"
|
android:hint="@string/hint_login_password"
|
||||||
android:inputType="textPassword"/>
|
android:inputType="textPassword" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- “Forgot password” link -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_forgetPassword"
|
android:id="@+id/tv_forgetPassword"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/forget_password"
|
android:text="@string/forget_password"
|
||||||
android:textColor="@android:color/holo_red_light"
|
|
||||||
android:textAlignment="textEnd"
|
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
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btn_login"
|
android:id="@+id/btn_login"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/login"
|
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" />
|
||||||
|
|
||||||
|
<!-- “Don’t have an account?” row (kept as LinearLayout) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/ll_signup_row"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center"
|
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
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/no_account"/>
|
android:text="@string/no_account" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_registrasi"
|
android:id="@+id/tv_registrasi"
|
||||||
@ -99,7 +138,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/signup"
|
android:text="@string/signup"
|
||||||
android:textColor="@color/blue1"
|
android:textColor="@color/blue1"
|
||||||
android:textStyle="bold"/>
|
android:textStyle="bold" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
tools:context=".ui.profile.mystore.MyStoreActivity">
|
tools:context=".ui.profile.mystore.MyStoreActivity">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/header"
|
android:id="@+id/headerMyStore"
|
||||||
layout="@layout/header" />
|
layout="@layout/header" />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@ -422,6 +422,7 @@
|
|||||||
android:background="@color/black_50"/>
|
android:background="@color/black_50"/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:visibility="gone"
|
||||||
android:id="@+id/layout_help"
|
android:id="@+id/layout_help"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/header"
|
android:id="@+id/headerListProduct"
|
||||||
layout="@layout/header" />
|
layout="@layout/header" />
|
||||||
|
|
||||||
<!-- Search Bar -->
|
<!-- Search Bar -->
|
||||||
|
@ -38,5 +38,6 @@
|
|||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toBottomOf="@id/linear_shipment"
|
app:layout_constraintTop_toBottomOf="@id/linear_shipment"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -33,4 +33,23 @@
|
|||||||
tools:listitem="@layout/item_chat"
|
tools:listitem="@layout/item_chat"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
|
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>
|
</LinearLayout>
|
@ -75,7 +75,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Didn't receive the code? " />
|
android:text="Belum menerima kode? " />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_resend_otp"
|
android:id="@+id/tv_resend_otp"
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
android:id="@+id/sv_address_register"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/btn_register"
|
app:layout_constraintBottom_toTopOf="@id/btn_register"
|
||||||
@ -149,7 +150,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="Kecamatan / Desa"
|
android:text="Kecamatan"
|
||||||
android:textColor="@android:color/black"
|
android:textColor="@android:color/black"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
@ -157,18 +158,61 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:hint="Isi Kecamatan / Desa"
|
android:hint="Pilih Kecamatan"
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<AutoCompleteTextView
|
||||||
android:id="@+id/et_kecamatan"
|
android:id="@+id/autoCompleteKecamatan"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="true"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp" />
|
||||||
android:inputType="textCapWords" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</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 -->
|
<!-- Kode Pos -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -196,7 +240,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:background="@drawable/bg_button_outline"
|
android:background="@drawable/bg_button_outline"
|
||||||
android:text="Previous"
|
android:text="Kembali"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@color/blue1"
|
android:textColor="@color/blue1"
|
||||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
|
||||||
@ -214,7 +258,8 @@
|
|||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sv_address_register"/>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/progress_bar"
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 11 KiB |
@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ecommerce_serang</string>
|
<string name="app_name">Bisa UMKM</string>
|
||||||
|
|
||||||
|
|
||||||
<!--Placeholder-->
|
<!--Placeholder-->
|
||||||
@ -118,12 +118,11 @@
|
|||||||
|
|
||||||
<!-- Cancellation Reasons -->
|
<!-- Cancellation Reasons -->
|
||||||
<string-array name="cancellation_reasons">
|
<string-array name="cancellation_reasons">
|
||||||
<item>Found a better price elsewhere</item>
|
<item>Menemukan harga yang lebih baik</item>
|
||||||
<item>Changed my mind about the product</item>
|
<item>Berubah pikiran dengan pilihan produk</item>
|
||||||
<item>Ordered the wrong item</item>
|
<item>Kesalahan membeli produk</item>
|
||||||
<item>Shipping time is too long</item>
|
<item>Alasan keuangan</item>
|
||||||
<item>Financial reasons</item>
|
<item>Lainnya</item>
|
||||||
<item>Other reason</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<!-- Chat Activity -->
|
<!-- Chat Activity -->
|
||||||
|