add edit profile activity

This commit is contained in:
shaulascr
2025-05-14 02:52:59 +07:00
parent 79c32fc5ee
commit c956e54166
10 changed files with 909 additions and 12 deletions

View File

@ -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" />

View File

@ -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
)

View File

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

View File

@ -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) {

View File

@ -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"
}
}

View File

@ -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()
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}

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