diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8e9559d..24db662 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,6 +29,9 @@
android:theme="@style/Theme.Ecommerce_serang"
android:usesCleartextTraffic="true"
tools:targetApi="31">
+
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/EditProfileResponse.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/EditProfileResponse.kt
new file mode 100644
index 0000000..ad0e3c9
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/EditProfileResponse.kt
@@ -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
+)
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
index efac13b..e1c09b7 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/api/retrofit/ApiService.kt
@@ -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
+ @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
+
@GET("order/{status}")
suspend fun getOrderList(
@Path("status") status: String
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt
index 835035c..9ab9afa 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/OrderRepository.kt
@@ -404,7 +404,7 @@ class OrderRepository(private val apiService: ApiService) {
suspend fun confirmOrderCompleted(request: CompletedOrderRequest): Result {
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) {
diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
index dfcd46b..2abd032 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/UserRepository.kt
@@ -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 {
+ 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"
+ }
+
}
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/DetailProfileActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/DetailProfileActivity.kt
index 03489a0..3a5f99e 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/DetailProfileActivity.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/DetailProfileActivity.kt
@@ -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()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/profile/editprofile/EditProfileCustActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/editprofile/EditProfileCustActivity.kt
new file mode 100644
index 0000000..9a7f3e2
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/ui/profile/editprofile/EditProfileCustActivity.kt
@@ -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,
+ 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"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/FileUtils.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/FileUtils.kt
new file mode 100644
index 0000000..facbdfc
--- /dev/null
+++ b/app/src/main/java/com/alya/ecommerce_serang/utils/FileUtils.kt
@@ -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"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt
index 2500b1f..3a08b14 100644
--- a/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt
+++ b/app/src/main/java/com/alya/ecommerce_serang/utils/viewmodel/ProfileViewModel.kt
@@ -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()
val errorMessage : LiveData = _errorMessage
+ private val _editProfileResult = MutableLiveData>()
+ val editProfileResult: LiveData> = _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"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_edit_profile_cust.xml b/app/src/main/res/layout/activity_edit_profile_cust.xml
new file mode 100644
index 0000000..97589d7
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_profile_cust.xml
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file