call detail product and profile

This commit is contained in:
shaulascr
2025-03-25 17:17:28 +07:00
parent 726b5ab3d3
commit 6239745d82
14 changed files with 303 additions and 28 deletions

View File

@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName
data class UserProfile( data class UserProfile(
@field:SerializedName("image") @field:SerializedName("image")
val image: Any?, val image: String? = null,
@field:SerializedName("role") @field:SerializedName("role")
val role: String, val role: String,

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response
import com.google.gson.annotations.SerializedName
data class AllStoreResponse(
@field:SerializedName("store")
val store: AllStore,
@field:SerializedName("message")
val message: String
)
data class AllStore(
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("description")
val description: String,
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: Any,
@field:SerializedName("status")
val status: String
)

View File

@ -0,0 +1,33 @@
package com.alya.ecommerce_serang.data.api.response
import com.google.gson.annotations.SerializedName
data class DetailStoreProductResponse(
@field:SerializedName("store")
val store: StoreProduct,
@field:SerializedName("message")
val message: String
)
data class StoreProduct(
@field:SerializedName("store_name")
val storeName: String,
@field:SerializedName("description")
val description: String,
@field:SerializedName("store_type")
val storeType: String,
@field:SerializedName("store_location")
val storeLocation: String,
@field:SerializedName("store_image")
val storeImage: Any,
@field:SerializedName("status")
val status: String
)

View File

@ -17,7 +17,7 @@ data class Product(
val storeId: Int, val storeId: Int,
@field:SerializedName("image") @field:SerializedName("image")
val image: String, val image: String? = null,
@field:SerializedName("rating") @field:SerializedName("rating")
val rating: String, val rating: String,

View File

@ -8,6 +8,7 @@ import com.alya.ecommerce_serang.data.api.response.CategoryResponse
import com.alya.ecommerce_serang.data.api.response.LoginResponse import com.alya.ecommerce_serang.data.api.response.LoginResponse
import com.alya.ecommerce_serang.data.api.response.OtpResponse import com.alya.ecommerce_serang.data.api.response.OtpResponse
import com.alya.ecommerce_serang.data.api.response.ProductResponse import com.alya.ecommerce_serang.data.api.response.ProductResponse
import com.alya.ecommerce_serang.data.api.response.ProfileResponse
import com.alya.ecommerce_serang.data.api.response.RegisterResponse import com.alya.ecommerce_serang.data.api.response.RegisterResponse
import com.alya.ecommerce_serang.data.api.response.ReviewProductResponse import com.alya.ecommerce_serang.data.api.response.ReviewProductResponse
import com.alya.ecommerce_serang.data.api.response.StoreResponse import com.alya.ecommerce_serang.data.api.response.StoreResponse
@ -51,6 +52,10 @@ interface ApiService {
@Path("id") productId: Int @Path("id") productId: Int
): Response<ProductResponse> ): Response<ProductResponse>
@GET("profile")
suspend fun getUserProfile(): Response<ProfileResponse>
@GET("mystore") @GET("mystore")
fun getStore (): Call<StoreResponse> fun getStore (): Call<StoreResponse>
} }

View File

@ -37,6 +37,7 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
response.body() response.body()
} else { } else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
null null
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -70,6 +71,7 @@ class ProductRepository(private val apiService: ApiService) {
if (response.isSuccessful) { if (response.isSuccessful) {
response.body()?.reviews // Ambil daftar review dari response response.body()?.reviews // Ambil daftar review dari response
} else { } else {
Log.e("ProductRepository", "Error: ${response.errorBody()?.string()}")
null null
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.data.repository
import com.alya.ecommerce_serang.data.api.dto.LoginRequest import com.alya.ecommerce_serang.data.api.dto.LoginRequest
import com.alya.ecommerce_serang.data.api.dto.OtpRequest import com.alya.ecommerce_serang.data.api.dto.OtpRequest
import com.alya.ecommerce_serang.data.api.dto.RegisterRequest import com.alya.ecommerce_serang.data.api.dto.RegisterRequest
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.response.LoginResponse import com.alya.ecommerce_serang.data.api.response.LoginResponse
import com.alya.ecommerce_serang.data.api.response.OtpResponse import com.alya.ecommerce_serang.data.api.response.OtpResponse
import com.alya.ecommerce_serang.data.api.retrofit.ApiService import com.alya.ecommerce_serang.data.api.retrofit.ApiService
@ -42,6 +43,23 @@ class UserRepository(private val apiService: ApiService) {
Result.Error(e) Result.Error(e)
} }
} }
suspend fun fetchUserProfile(): Result<UserProfile?> {
return try {
val response = apiService.getUserProfile()
if (response.isSuccessful) {
response.body()?.user?.let {
Result.Success(it) // ✅ Returning only UserProfile
} ?: Result.Error(Exception("User data not found"))
} else {
Result.Error(Exception("Error fetching profile: ${response.code()}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
} }
sealed class Result<out T> { sealed class Result<out T> {

View File

@ -107,7 +107,7 @@ class HomeFragment : Fragment() {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
binding.error.root.isVisible = false binding.error.root.isVisible = false
binding.home.isVisible = true binding.home.isVisible = true
productAdapter?.updateLimitedProducts(state.products) productAdapter?.updateLimitedProducts(state.products) // Ensure productAdapter is initialized
} }
is HomeUiState.Error -> { is HomeUiState.Error -> {
binding.loading.root.isVisible = false binding.loading.root.isVisible = false
@ -123,7 +123,7 @@ class HomeFragment : Fragment() {
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.categories.collect { categories -> viewModel.categories.collect { categories ->
Log.d("Categories", "Updated Categories: $categories") Log.d("Categories", "Updated Categories: $categories")

View File

@ -4,13 +4,16 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.response.Product
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.ProductRepository import com.alya.ecommerce_serang.data.repository.ProductRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding import com.alya.ecommerce_serang.databinding.ActivityDetailProductBinding
import com.alya.ecommerce_serang.ui.home.HomeViewModel
import com.alya.ecommerce_serang.utils.BaseViewModelFactory import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
class DetailProductActivity : AppCompatActivity() { class DetailProductActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProductBinding private lateinit var binding: ActivityDetailProductBinding
@ -21,7 +24,7 @@ class DetailProductActivity : AppCompatActivity() {
BaseViewModelFactory { BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager) val apiService = ApiConfig.getApiService(sessionManager)
val productRepository = ProductRepository(apiService) val productRepository = ProductRepository(apiService)
HomeViewModel(productRepository) ProductViewModel(productRepository)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -51,21 +54,42 @@ class DetailProductActivity : AppCompatActivity() {
if (product != null) { if (product != null) {
Log.d("ProductDetail", "Name: ${product.productName}, Price: ${product.price}") Log.d("ProductDetail", "Name: ${product.productName}, Price: ${product.price}")
// Update UI here, e.g., show in a TextView or ImageView // Update UI here, e.g., show in a TextView or ImageView
binding.tvProductName.text = product.productName viewModel.loadProductDetail(productId)
binding.tvPrice.text = product.price
binding.tvSold.text = product.totalSold.toString()
binding.tvRating.text = product.rating
binding.tvWeight.text = product.weight.toString()
binding.tvStock.text = product.stock.toString()
binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description
binding.tvSellerName.text = product.storeId.toString()
} else { } else {
Log.e("ProductDetail", "Failed to fetch product details") Log.e("ProductDetail", "Failed to fetch product details")
} }
} }
observeProductDetail()
}
private fun observeProductDetail() {
viewModel.productDetail.observe(this) { product ->
product?.let { updateUI(it) }
}
}
private fun updateUI(product: Product){
binding.tvProductName.text = product.productName
binding.tvPrice.text = product.price
binding.tvSold.text = product.totalSold.toString()
binding.tvRating.text = product.rating
binding.tvWeight.text = product.weight.toString()
binding.tvStock.text = product.stock.toString()
binding.tvCategory.text = product.productCategory
binding.tvDescription.text = product.description
binding.tvSellerName.text = product.storeId.toString()
val fullImageUrl = when (val img = product.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image // Default image for null
}
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
Glide.with(this)
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(binding.ivProductImage)
} }
} }

View File

@ -1,21 +1,85 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import com.alya.ecommerce_serang.data.api.dto.UserProfile
import androidx.core.view.WindowInsetsCompat import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.R import com.alya.ecommerce_serang.data.api.retrofit.ApiService
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.ActivityDetailProfileBinding
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class DetailProfileActivity : AppCompatActivity() { class DetailProfileActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailProfileBinding
private lateinit var apiService: ApiService
private lateinit var sessionManager: SessionManager
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ProfileViewModel(userRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityDetailProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
sessionManager = SessionManager(this)
apiService = ApiConfig.getApiService(sessionManager)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_detail_profile) // ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> // val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) // v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) // insets
insets // }
viewModel.loadUserProfile()
viewModel.userProfile.observe(this){ user ->
user?.let { updateProfile(it) }
}
viewModel.errorMessage.observe(this) { error ->
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
}
}
private fun updateProfile(user: UserProfile){
binding.tvNameUser.setText(user.name.toString())
binding.tvUsername.setText(user.username)
binding.tvEmailUser.setText(user.email)
binding.tvDateBirth.setText(formatDate(user.birthDate))
binding.tvNumberPhoneUser.setText(user.phone)
if (user.image != null && user.image is String) {
Glide.with(this)
.load(user.image)
.into(binding.profileImage)
}
}
private fun formatDate(dateString: String): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) //from json
inputFormat.timeZone = TimeZone.getTimeZone("UTC") //get timezone
val outputFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()) // new format
val date = inputFormat.parse(dateString) // Parse from json format
outputFormat.format(date!!) // convert to new format
} catch (e: Exception) {
dateString // Return original if error occurs
} }
} }
} }

View File

@ -1,22 +1,43 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import android.content.Intent
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
import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
import com.alya.ecommerce_serang.R
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
import com.alya.ecommerce_serang.data.repository.UserRepository
import com.alya.ecommerce_serang.databinding.FragmentProfileBinding import com.alya.ecommerce_serang.databinding.FragmentProfileBinding
import com.alya.ecommerce_serang.ui.profile.mystore.TokoSayaActivity
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
import com.alya.ecommerce_serang.utils.SessionManager
import com.bumptech.glide.Glide
class ProfileFragment : Fragment() { class ProfileFragment : Fragment() {
private var _binding: FragmentProfileBinding? = null private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var viewModel: ProfileViewModel private lateinit var sessionManager: SessionManager
private val viewModel: ProfileViewModel by viewModels {
BaseViewModelFactory {
val apiService = ApiConfig.getApiService(sessionManager)
val userRepository = UserRepository(apiService)
ProfileViewModel(userRepository)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sessionManager = SessionManager(requireContext())
// TODO: Use the ViewModel
} }
override fun onCreateView( override fun onCreateView(
@ -26,4 +47,49 @@ class ProfileFragment : Fragment() {
_binding = FragmentProfileBinding.inflate(inflater, container, false) _binding = FragmentProfileBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeUserProfile()
viewModel.loadUserProfile()
binding.cardBukaToko.setOnClickListener{
val intentBuka = Intent(requireContext(), TokoSayaActivity::class.java)
startActivity(intentBuka)
}
binding.btnDetailProfile.setOnClickListener{
val intentDetail = Intent(requireContext(), DetailProfileActivity::class.java)
startActivity(intentDetail)
}
}
private fun observeUserProfile() {
viewModel.userProfile.observe(viewLifecycleOwner) { user ->
user?.let { updateUI(it) }
}
viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage ->
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
}
private fun updateUI(user: UserProfile) = with(binding){
val fullImageUrl = when (val img = user.image) {
is String -> {
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
}
else -> R.drawable.placeholder_image // Default image for null
}
Log.d("ProductAdapter", "Loading image: $fullImageUrl")
tvName.text = user.name.toString()
tvUsername.text = user.username.toString()
// Load image using Glide
Glide.with(requireContext())
.load(fullImageUrl)
.placeholder(R.drawable.placeholder_image)
.into(profileImage)
}
} }

View File

@ -1,7 +1,28 @@
package com.alya.ecommerce_serang.ui.profile package com.alya.ecommerce_serang.ui.profile
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alya.ecommerce_serang.data.api.dto.UserProfile
import com.alya.ecommerce_serang.data.repository.Result
import com.alya.ecommerce_serang.data.repository.UserRepository
import kotlinx.coroutines.launch
class ProfileViewModel : ViewModel() { class ProfileViewModel(private val userRepository: UserRepository) : ViewModel() {
// TODO: Implement the ViewModel private val _userProfile = MutableLiveData<UserProfile?>()
val userProfile: LiveData<UserProfile?> = _userProfile
private val _errorMessage = MutableLiveData<String>()
val errorMessage : LiveData<String> = _errorMessage
fun loadUserProfile(){
viewModelScope.launch {
when (val result = userRepository.fetchUserProfile()){
is Result.Success -> _userProfile.postValue(result.data)
is Result.Error -> _errorMessage.postValue(result.exception.message ?: "Unknown Error")
is Result.Loading -> null
}
}
}
} }

View File

@ -104,6 +104,7 @@
app:layout_constraintTop_toBottomOf="@id/card_profile"> app:layout_constraintTop_toBottomOf="@id/card_profile">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tv_name_user"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Nama" android:hint="Nama"
@ -120,6 +121,7 @@
app:layout_constraintTop_toBottomOf="@id/til_nama"> app:layout_constraintTop_toBottomOf="@id/til_nama">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tv_username"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Username" android:hint="Username"
@ -136,6 +138,7 @@
app:layout_constraintTop_toBottomOf="@id/til_username"> app:layout_constraintTop_toBottomOf="@id/til_username">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tv_email_user"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Email" android:hint="Email"
@ -152,6 +155,7 @@
app:layout_constraintTop_toBottomOf="@id/til_email"> app:layout_constraintTop_toBottomOf="@id/til_email">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tv_number_phone_user"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Nomor Handphone" android:hint="Nomor Handphone"
@ -168,6 +172,7 @@
app:layout_constraintTop_toBottomOf="@id/til_nomor_handphone"> app:layout_constraintTop_toBottomOf="@id/til_nomor_handphone">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tv_date_birth"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="Tanggal Lahir" android:hint="Tanggal Lahir"

View File

@ -45,6 +45,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:text="Detail Profil" android:text="Detail Profil"
android:clickable="true"
android:focusable="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/profileImage" /> app:layout_constraintTop_toTopOf="@id/profileImage" />
@ -55,6 +57,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="true"
android:focusable="true"
app:cardElevation="4dp" app:cardElevation="4dp"
app:layout_constraintTop_toBottomOf="@id/profileImage"> app:layout_constraintTop_toBottomOf="@id/profileImage">