mirror of
https://github.com/shaulascr/ecommerce_serang.git
synced 2025-08-10 09:22:21 +00:00
add edit profile activity
This commit is contained in:
@ -29,6 +29,9 @@
|
||||
android:theme="@style/Theme.Ecommerce_serang"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.profile.editprofile.EditProfileCustActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".ui.order.history.detailorder.DetailOrderStatusActivity"
|
||||
android:exported="false" />
|
||||
|
@ -0,0 +1,9 @@
|
||||
package com.alya.ecommerce_serang.data.api.response.customer.profile
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class EditProfileResponse(
|
||||
|
||||
@field:SerializedName("message")
|
||||
val message: String
|
||||
)
|
@ -41,6 +41,7 @@ import com.alya.ecommerce_serang.data.api.response.customer.product.ReviewProduc
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.product.StoreResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.AddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.CreateAddressResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.ProfileResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.order.AddEvidenceResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.order.ComplaintResponse
|
||||
@ -129,6 +130,17 @@ interface ApiService {
|
||||
@Part evidence: MultipartBody.Part
|
||||
): Response<AddEvidenceResponse>
|
||||
|
||||
@Multipart
|
||||
@PUT("profile/edit")
|
||||
suspend fun editProfileCustomer(
|
||||
@Part("username") username: RequestBody,
|
||||
@Part("name") name: RequestBody,
|
||||
@Part("phone") phone: RequestBody,
|
||||
@Part("birth_date") birthDate: RequestBody,
|
||||
@Part userimg: MultipartBody.Part,
|
||||
@Part("email") email: RequestBody
|
||||
): Response<EditProfileResponse>
|
||||
|
||||
@GET("order/{status}")
|
||||
suspend fun getOrderList(
|
||||
@Path("status") status: String
|
||||
|
@ -404,7 +404,7 @@ class OrderRepository(private val apiService: ApiService) {
|
||||
|
||||
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result<CompletedOrderResponse> {
|
||||
return try {
|
||||
Log.d("OrderRepository", "Cinfroming order request completed: $request")
|
||||
Log.d("OrderRepository", "Conforming order request completed: $request")
|
||||
val response = apiService.confirmOrder(request)
|
||||
|
||||
if(response.isSuccessful) {
|
||||
|
@ -1,15 +1,21 @@
|
||||
package com.alya.ecommerce_serang.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.alya.ecommerce_serang.data.api.dto.LoginRequest
|
||||
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.UserProfile
|
||||
import com.alya.ecommerce_serang.data.api.response.auth.LoginResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.auth.OtpResponse
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||
import com.alya.ecommerce_serang.utils.FileUtils
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class UserRepository(private val apiService: ApiService) {
|
||||
|
||||
//post data without message/response
|
||||
suspend fun requestOtpRep(email: String): OtpResponse {
|
||||
return apiService.getOTP(OtpRequest(email))
|
||||
@ -56,6 +62,75 @@ class UserRepository(private val apiService: ApiService) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun editProfileCust(
|
||||
context: Context,
|
||||
username: String,
|
||||
name: String,
|
||||
phone: String,
|
||||
birthDate: String,
|
||||
email: String,
|
||||
imageUri: Uri?
|
||||
): Result<EditProfileResponse> {
|
||||
return try {
|
||||
// Log the data being sent
|
||||
Log.d(TAG, "Edit Profile - Username: $username, Name: $name, Phone: $phone, Birth Date: $birthDate, Email: $email")
|
||||
Log.d(TAG, "Image URI: $imageUri")
|
||||
|
||||
// Create RequestBody objects for text fields
|
||||
val usernameRequestBody = username.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val nameRequestBody = name.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val phoneRequestBody = phone.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val birthDateRequestBody = birthDate.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
val emailRequestBody = email.toRequestBody("text/plain".toMediaTypeOrNull())
|
||||
|
||||
// Create MultipartBody.Part for the image
|
||||
val imagePart = if (imageUri != null) {
|
||||
// Create a temporary file from the URI using the utility class
|
||||
val imageFile = FileUtils.createTempFileFromUri(context, imageUri, "profile")
|
||||
if (imageFile != null) {
|
||||
// Create MultipartBody.Part from the file
|
||||
FileUtils.createMultipartFromFile("userimg", imageFile)
|
||||
} else {
|
||||
// Fallback to empty part
|
||||
FileUtils.createEmptyMultipart("userimg")
|
||||
}
|
||||
} else {
|
||||
// No image selected, use empty part
|
||||
FileUtils.createEmptyMultipart("userimg")
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
val response = apiService.editProfileCustomer(
|
||||
username = usernameRequestBody,
|
||||
name = nameRequestBody,
|
||||
phone = phoneRequestBody,
|
||||
birthDate = birthDateRequestBody,
|
||||
userimg = imagePart,
|
||||
email = emailRequestBody
|
||||
)
|
||||
|
||||
// Process the response
|
||||
if (response.isSuccessful) {
|
||||
val editResponse = response.body()
|
||||
if (editResponse != null) {
|
||||
Log.d(TAG, "Edit profile success: ${editResponse.message}")
|
||||
Result.Success(editResponse)
|
||||
} else {
|
||||
Log.e(TAG, "Response body is null")
|
||||
Result.Error(Exception("Empty response from server"))
|
||||
}
|
||||
} else {
|
||||
val errorBody = response.errorBody()?.string() ?: "Unknown Error"
|
||||
Log.e(TAG, "Error editing profile: $errorBody")
|
||||
Result.Error(Exception(errorBody))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception in editProfileCust: ${e.message}")
|
||||
e.printStackTrace()
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// suspend fun sendChatMessage(
|
||||
// storeId: Int,
|
||||
// message: String,
|
||||
@ -152,6 +227,10 @@ class UserRepository(private val apiService: ApiService) {
|
||||
// }
|
||||
// }
|
||||
|
||||
companion object{
|
||||
private const val TAG = "UserRepository"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,29 @@
|
||||
package com.alya.ecommerce_serang.ui.profile
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||
import com.alya.ecommerce_serang.databinding.ActivityDetailProfileBinding
|
||||
import com.alya.ecommerce_serang.ui.profile.editprofile.EditProfileCustActivity
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.gson.Gson
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
@ -24,6 +32,8 @@ class DetailProfileActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityDetailProfileBinding
|
||||
private lateinit var apiService: ApiService
|
||||
private lateinit var sessionManager: SessionManager
|
||||
private var currentUserProfile: UserProfile? = null
|
||||
|
||||
|
||||
private val viewModel: ProfileViewModel by viewModels {
|
||||
BaseViewModelFactory {
|
||||
@ -33,6 +43,15 @@ class DetailProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val editProfileLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
// Refresh profile after edit
|
||||
viewModel.loadUserProfile()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityDetailProfileBinding.inflate(layoutInflater)
|
||||
@ -42,24 +61,61 @@ class DetailProfileActivity : AppCompatActivity() {
|
||||
apiService = ApiConfig.getApiService(sessionManager)
|
||||
|
||||
enableEdgeToEdge()
|
||||
// ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
// val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
// v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
// insets
|
||||
// }
|
||||
|
||||
setupClickListeners()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(
|
||||
systemBars.left,
|
||||
systemBars.top,
|
||||
systemBars.right,
|
||||
systemBars.bottom
|
||||
)
|
||||
windowInsets
|
||||
}
|
||||
|
||||
viewModel.loadUserProfile()
|
||||
|
||||
viewModel.userProfile.observe(this){ user ->
|
||||
user?.let { updateProfile(it) }
|
||||
viewModel.userProfile.observe(this) { user ->
|
||||
Log.d("DetailProfileActivity", "Observed userProfile: $user")
|
||||
user?.let {
|
||||
updateProfile(it)
|
||||
} ?: run {
|
||||
Log.e("DetailProfileActivity", "Received null user profile from ViewModel")
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.errorMessage.observe(this) { error ->
|
||||
Log.e("DetailProfileActivity", "Error from ViewModel: $error")
|
||||
Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(user: UserProfile){
|
||||
private fun setupClickListeners() {
|
||||
binding.btnBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.btnUbahProfil.setOnClickListener {
|
||||
currentUserProfile?.let { profile ->
|
||||
val gson = Gson()
|
||||
val userProfileJson = gson.toJson(currentUserProfile)
|
||||
val intent = Intent(this, EditProfileCustActivity::class.java).apply {
|
||||
putExtra("user_profile_json", userProfileJson)
|
||||
}
|
||||
editProfileLauncher.launch(intent)
|
||||
} ?: run {
|
||||
Toast.makeText(this, "Profile data is not available", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(user: UserProfile) {
|
||||
Log.d("DetailProfileActivity", "updateProfile called with user: $user")
|
||||
|
||||
// Store the user profile for later use
|
||||
currentUserProfile = user
|
||||
|
||||
binding.tvNameUser.setText(user.name.toString())
|
||||
binding.tvUsername.setText(user.username)
|
||||
@ -69,9 +125,16 @@ class DetailProfileActivity : AppCompatActivity() {
|
||||
Log.d("ProfileActivity", "Formatted Birth Date: ${formatDate(user.birthDate)}")
|
||||
binding.tvNumberPhoneUser.setText(user.phone)
|
||||
|
||||
if (user.image != null && user.image is String) {
|
||||
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
|
||||
}
|
||||
|
||||
if (fullImageUrl != null && fullImageUrl is String) {
|
||||
Glide.with(this)
|
||||
.load(user.image)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.baseline_account_circle_24)
|
||||
.into(binding.profileImage)
|
||||
}
|
||||
@ -93,4 +156,10 @@ class DetailProfileActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Refresh profile data when returning to this screen
|
||||
viewModel.loadUserProfile()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,390 @@
|
||||
package com.alya.ecommerce_serang.ui.profile.editprofile
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.alya.ecommerce_serang.BuildConfig.BASE_URL
|
||||
import com.alya.ecommerce_serang.R
|
||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiConfig
|
||||
import com.alya.ecommerce_serang.data.api.retrofit.ApiService
|
||||
import com.alya.ecommerce_serang.data.repository.Result
|
||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||
import com.alya.ecommerce_serang.databinding.ActivityEditProfileCustBinding
|
||||
import com.alya.ecommerce_serang.utils.BaseViewModelFactory
|
||||
import com.alya.ecommerce_serang.utils.SessionManager
|
||||
import com.alya.ecommerce_serang.utils.viewmodel.ProfileViewModel
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.gson.Gson
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class EditProfileCustActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityEditProfileCustBinding
|
||||
private lateinit var apiService: ApiService
|
||||
private lateinit var sessionManager: SessionManager
|
||||
private var selectedImageUri: Uri? = null
|
||||
|
||||
private val viewModel: ProfileViewModel by viewModels {
|
||||
BaseViewModelFactory {
|
||||
val apiService = ApiConfig.getApiService(sessionManager)
|
||||
val userRepository = UserRepository(apiService)
|
||||
ProfileViewModel(userRepository)
|
||||
}
|
||||
}
|
||||
|
||||
private val getContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val data: Intent? = result.data
|
||||
data?.data?.let {
|
||||
selectedImageUri = it
|
||||
|
||||
val fullImageUrl = when (val img = selectedImageUri.toString()) {
|
||||
is String -> {
|
||||
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||
}
|
||||
else -> R.drawable.placeholder_image // Default image for null
|
||||
}
|
||||
|
||||
Glide.with(this)
|
||||
.load(fullImageUrl)
|
||||
.into(binding.profileImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityEditProfileCustBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
sessionManager = SessionManager(this)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Apply insets to your root layout
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.setPadding(
|
||||
systemBars.left,
|
||||
systemBars.top,
|
||||
systemBars.right,
|
||||
systemBars.bottom
|
||||
)
|
||||
windowInsets
|
||||
}
|
||||
|
||||
val userProfileJson = intent.getStringExtra("user_profile_json")
|
||||
val userProfile = if (userProfileJson != null) {
|
||||
val gson = Gson()
|
||||
gson.fromJson(userProfileJson, UserProfile::class.java)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
userProfile?.let {
|
||||
populateFields(it)
|
||||
|
||||
setupClickListeners()
|
||||
observeViewModel()
|
||||
}
|
||||
}
|
||||
private fun populateFields(profile: UserProfile) {
|
||||
binding.etNameUser.setText(profile.name)
|
||||
binding.etUsername.setText(profile.username)
|
||||
binding.etEmailUser.setText(profile.email)
|
||||
binding.etNumberPhoneUser.setText(profile.phone)
|
||||
|
||||
// Format birth date for display
|
||||
profile.birthDate?.let {
|
||||
binding.etDateBirth.setText(formatDate(it))
|
||||
}
|
||||
|
||||
val fullImageUrl = when (val img = profile.image) {
|
||||
is String -> {
|
||||
if (img.startsWith("/")) BASE_URL + img.substring(1) else img
|
||||
}
|
||||
else -> R.drawable.placeholder_image // Default image for null
|
||||
}
|
||||
|
||||
// Load profile image
|
||||
if (fullImageUrl != null && fullImageUrl is String) {
|
||||
Glide.with(this)
|
||||
.load(fullImageUrl)
|
||||
.placeholder(R.drawable.baseline_account_circle_24)
|
||||
.into(binding.profileImage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.btnBack.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.editIcon.setOnClickListener {
|
||||
openImagePicker()
|
||||
}
|
||||
|
||||
binding.tvSelectImage.setOnClickListener {
|
||||
openImagePicker()
|
||||
}
|
||||
|
||||
binding.etDateBirth.setOnClickListener {
|
||||
showDatePicker()
|
||||
}
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
saveProfile()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openImagePicker() {
|
||||
// Check for permission first
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
REQUEST_STORAGE_PERMISSION
|
||||
)
|
||||
} else {
|
||||
launchImagePicker()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchImagePicker() {
|
||||
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
|
||||
getContent.launch(intent)
|
||||
}
|
||||
|
||||
private fun showDatePicker() {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
// If there's already a date in the field, parse it
|
||||
val dateText = binding.etDateBirth.text.toString()
|
||||
if (dateText.isNotEmpty() && dateText != "N/A" && dateText != "Invalid Date") {
|
||||
try {
|
||||
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
|
||||
val date = displayFormat.parse(dateText)
|
||||
date?.let {
|
||||
calendar.time = it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing date: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
val year = calendar.get(Calendar.YEAR)
|
||||
val month = calendar.get(Calendar.MONTH)
|
||||
val day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
|
||||
val datePickerDialog = DatePickerDialog(
|
||||
this,
|
||||
{ _, selectedYear, selectedMonth, selectedDay ->
|
||||
calendar.set(selectedYear, selectedMonth, selectedDay)
|
||||
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
|
||||
val formattedDate = displayFormat.format(calendar.time)
|
||||
binding.etDateBirth.setText(formattedDate)
|
||||
},
|
||||
year, month, day
|
||||
)
|
||||
datePickerDialog.show()
|
||||
}
|
||||
|
||||
private fun saveProfile() {
|
||||
val name = binding.etNameUser.text.toString()
|
||||
val username = binding.etUsername.text.toString()
|
||||
val email = binding.etEmailUser.text.toString()
|
||||
val phone = binding.etNumberPhoneUser.text.toString()
|
||||
val displayDate = binding.etDateBirth.text.toString()
|
||||
|
||||
if (name.isEmpty() || username.isEmpty() || email.isEmpty() || phone.isEmpty() || displayDate.isEmpty()) {
|
||||
Toast.makeText(this, "Semua field harus diisi", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Convert date to server format
|
||||
val serverBirthDate = convertToServerDateFormat(displayDate)
|
||||
|
||||
Log.d(TAG, "Starting profile save with direct method")
|
||||
Log.d(TAG, "Selected image URI: $selectedImageUri")
|
||||
|
||||
// Disable the button to prevent multiple clicks
|
||||
binding.btnSave.isEnabled = false
|
||||
|
||||
// Call the repository method via ViewModel
|
||||
viewModel.editProfileDirect(
|
||||
context = this, // Pass context for file operations
|
||||
username = username,
|
||||
name = name,
|
||||
phone = phone,
|
||||
birthDate = serverBirthDate,
|
||||
email = email,
|
||||
imageUri = selectedImageUri
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRealPathFromURI(uri: Uri): String? {
|
||||
Log.d(TAG, "Getting real path from URI: $uri")
|
||||
|
||||
// Handle different URI schemes
|
||||
when {
|
||||
// File URI
|
||||
uri.scheme == "file" -> {
|
||||
val path = uri.path
|
||||
Log.d(TAG, "URI is file scheme, path: $path")
|
||||
return path
|
||||
}
|
||||
|
||||
// Content URI
|
||||
uri.scheme == "content" -> {
|
||||
try {
|
||||
val projection = arrayOf(MediaStore.Images.Media.DATA)
|
||||
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
|
||||
val path = cursor.getString(columnIndex)
|
||||
Log.d(TAG, "Found path from content URI: $path")
|
||||
return path
|
||||
} else {
|
||||
Log.e(TAG, "Cursor is empty")
|
||||
}
|
||||
} ?: Log.e(TAG, "Cursor is null")
|
||||
|
||||
// If the above fails, try the documented API way
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
// Create a temp file
|
||||
val fileName = getFileName(uri) ?: "temp_img_${System.currentTimeMillis()}.jpg"
|
||||
val tempFile = File(cacheDir, fileName)
|
||||
tempFile.outputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
Log.d(TAG, "Created temporary file: ${tempFile.absolutePath}")
|
||||
return tempFile.absolutePath
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error getting real path: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.e(TAG, "Could not get real path for URI: $uri")
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getFileName(uri: Uri): String? {
|
||||
var result: String? = null
|
||||
if (uri.scheme == "content") {
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val columnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
if (columnIndex >= 0) {
|
||||
result = cursor.getString(columnIndex)
|
||||
Log.d(TAG, "Found filename from content URI: $result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.path
|
||||
val cut = result?.lastIndexOf('/') ?: -1
|
||||
if (cut != -1) {
|
||||
result = result?.substring(cut + 1)
|
||||
}
|
||||
Log.d(TAG, "Extracted filename from path: $result")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun formatDate(dateString: String?): String {
|
||||
if (dateString.isNullOrEmpty()) return "N/A"
|
||||
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
|
||||
inputFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
val outputFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
|
||||
val date = inputFormat.parse(dateString)
|
||||
outputFormat.format(date ?: return "Invalid Date")
|
||||
} catch (e: Exception) {
|
||||
Log.e("ERROR", "Date parsing error: ${e.message}")
|
||||
"Invalid Date"
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToServerDateFormat(displayDate: String): String {
|
||||
return try {
|
||||
val displayFormat = SimpleDateFormat("dd-MM-yy", Locale.getDefault())
|
||||
val date = displayFormat.parse(displayDate) ?: return ""
|
||||
|
||||
val serverFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
serverFormat.format(date)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error converting date format: ${e.message}")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.editProfileResult.observe(this) { result ->
|
||||
when (result) {
|
||||
is com.alya.ecommerce_serang.data.repository.Result.Loading -> {
|
||||
// Show loading indicator
|
||||
binding.btnSave.isEnabled = false
|
||||
}
|
||||
is com.alya.ecommerce_serang.data.repository.Result.Success -> {
|
||||
// Show success message
|
||||
Toast.makeText(this, result.data.message, Toast.LENGTH_SHORT).show()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
is Result.Error -> {
|
||||
// Show error message
|
||||
Toast.makeText(this, result.exception.message ?: "Error updating profile", Toast.LENGTH_SHORT).show()
|
||||
binding.btnSave.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == REQUEST_STORAGE_PERMISSION && grantResults.isNotEmpty() && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
launchImagePicker()
|
||||
} else {
|
||||
Toast.makeText(this, "Permission needed to select image", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_STORAGE_PERMISSION = 100
|
||||
private const val TAG = "EditProfileCustActivity"
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package com.alya.ecommerce_serang.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object FileUtils {
|
||||
private const val TAG = "FileUtils"
|
||||
|
||||
/**
|
||||
* Creates a temporary file from a URI in the app's cache directory
|
||||
*/
|
||||
fun createTempFileFromUri(context: Context, uri: Uri, prefix: String = "temp"): File? {
|
||||
try {
|
||||
val fileExtension = getFileExtension(context, uri)
|
||||
val fileName = "${prefix}_${System.currentTimeMillis()}.$fileExtension"
|
||||
val tempFile = File(context.cacheDir, fileName)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
FileOutputStream(tempFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
return if (tempFile.exists() && tempFile.length() > 0) {
|
||||
Log.d(TAG, "Created temp file: ${tempFile.absolutePath}, size: ${tempFile.length()} bytes")
|
||||
tempFile
|
||||
} else {
|
||||
Log.e(TAG, "Created file is empty or doesn't exist")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error creating temp file: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension from a URI using ContentResolver
|
||||
*/
|
||||
fun getFileExtension(context: Context, uri: Uri): String {
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
return if (mimeType != null) {
|
||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg"
|
||||
} else {
|
||||
// Try to extract from the URI path
|
||||
val path = uri.toString()
|
||||
if (path.contains(".")) {
|
||||
path.substring(path.lastIndexOf(".") + 1)
|
||||
} else {
|
||||
"jpg" // Default extension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a MultipartBody.Part from a File for API requests
|
||||
*/
|
||||
fun createMultipartFromFile(paramName: String, file: File): MultipartBody.Part {
|
||||
val requestFile = file.asRequestBody(getMimeType(file).toMediaTypeOrNull())
|
||||
return MultipartBody.Part.createFormData(paramName, file.name, requestFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty MultipartBody.Part
|
||||
*/
|
||||
fun createEmptyMultipart(paramName: String): MultipartBody.Part {
|
||||
return MultipartBody.Part.createFormData(paramName, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MIME type for a file based on its extension
|
||||
*/
|
||||
fun getMimeType(file: File): String {
|
||||
return when (file.extension.lowercase()) {
|
||||
"jpg", "jpeg" -> "image/jpeg"
|
||||
"png" -> "image/png"
|
||||
"pdf" -> "application/pdf"
|
||||
else -> "application/octet-stream"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
package com.alya.ecommerce_serang.utils.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.alya.ecommerce_serang.data.api.dto.UserProfile
|
||||
import com.alya.ecommerce_serang.data.api.response.customer.profile.EditProfileResponse
|
||||
import com.alya.ecommerce_serang.data.repository.Result
|
||||
import com.alya.ecommerce_serang.data.repository.UserRepository
|
||||
import kotlinx.coroutines.launch
|
||||
@ -16,6 +20,9 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
|
||||
private val _errorMessage = MutableLiveData<String>()
|
||||
val errorMessage : LiveData<String> = _errorMessage
|
||||
|
||||
private val _editProfileResult = MutableLiveData<Result<EditProfileResponse>>()
|
||||
val editProfileResult: LiveData<Result<EditProfileResponse>> = _editProfileResult
|
||||
|
||||
fun loadUserProfile(){
|
||||
viewModelScope.launch {
|
||||
when (val result = userRepository.fetchUserProfile()){
|
||||
@ -25,4 +32,46 @@ class ProfileViewModel(private val userRepository: UserRepository) : ViewModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun editProfileDirect(
|
||||
context: Context,
|
||||
username: String,
|
||||
name: String,
|
||||
phone: String,
|
||||
birthDate: String,
|
||||
email: String,
|
||||
imageUri: Uri?
|
||||
) {
|
||||
_editProfileResult.value = Result.Loading
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Calling editProfileCust with direct parameters")
|
||||
val result = userRepository.editProfileCust(
|
||||
context = context,
|
||||
username = username,
|
||||
name = name,
|
||||
phone = phone,
|
||||
birthDate = birthDate,
|
||||
email = email,
|
||||
imageUri = imageUri
|
||||
)
|
||||
|
||||
_editProfileResult.value = result
|
||||
|
||||
// Reload user profile after successful update
|
||||
if (result is Result.Success) {
|
||||
Log.d(TAG, "Edit profile successful, reloading profile data")
|
||||
loadUserProfile()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in editProfileDirect: ${e.message}")
|
||||
e.printStackTrace()
|
||||
_editProfileResult.value = Result.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ProfileViewModel"
|
||||
}
|
||||
}
|
198
app/src/main/res/layout/activity_edit_profile_cust.xml
Normal file
198
app/src/main/res/layout/activity_edit_profile_cust.xml
Normal file
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.profile.editprofile.EditProfileCustActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/top_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_back_24"
|
||||
android:contentDescription="back"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_profile_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Edit Profil"
|
||||
android:textSize="20sp"
|
||||
android:fontFamily="@font/dmsans_bold"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card_profile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Profile Image -->
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/profile_image"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@drawable/baseline_account_circle_24"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<!-- Edit Icon -->
|
||||
<ImageView
|
||||
android:id="@+id/edit_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:background="@drawable/circle_background"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/profile_image"
|
||||
app:layout_constraintEnd_toEndOf="@id/profile_image"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:tint="@color/blue_500" />
|
||||
|
||||
<!-- Select Image Text -->
|
||||
<TextView
|
||||
android:id="@+id/tv_select_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Pilih Gambar"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/blue_500"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile_image"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_nama"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_profile">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_name_user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Nama"
|
||||
android:text="Nama Pengguna"
|
||||
android:inputType="textPersonName"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_nama">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Username"
|
||||
android:text="Username Pengguna"
|
||||
android:inputType="text"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_email"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_username">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_email_user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Email"
|
||||
android:text="Email Pengguna"
|
||||
android:inputType="textEmailAddress"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_nomor_handphone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_email">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_number_phone_user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Nomor Handphone"
|
||||
android:text="Nomor handphone pengguna"
|
||||
android:inputType="phone"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/til_tanggal_lahir"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_nomor_handphone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_date_birth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Tanggal Lahir"
|
||||
android:text="Tanggal Lahir Pengguna"
|
||||
android:inputType="date"
|
||||
android:focusable="false"
|
||||
android:clickable="true"/>
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Simpan"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/til_tanggal_lahir"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Reference in New Issue
Block a user