From c956e54166760aaaad20e39023b28efa8e529350 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Wed, 14 May 2025 02:52:59 +0700 Subject: [PATCH] add edit profile activity --- app/src/main/AndroidManifest.xml | 3 + .../customer/profile/EditProfileResponse.kt | 9 + .../data/api/retrofit/ApiService.kt | 12 + .../data/repository/OrderRepository.kt | 2 +- .../data/repository/UserRepository.kt | 81 +++- .../ui/profile/DetailProfileActivity.kt | 89 +++- .../editprofile/EditProfileCustActivity.kt | 390 ++++++++++++++++++ .../alya/ecommerce_serang/utils/FileUtils.kt | 88 ++++ .../utils/viewmodel/ProfileViewModel.kt | 49 +++ .../res/layout/activity_edit_profile_cust.xml | 198 +++++++++ 10 files changed, 909 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/alya/ecommerce_serang/data/api/response/customer/profile/EditProfileResponse.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/ui/profile/editprofile/EditProfileCustActivity.kt create mode 100644 app/src/main/java/com/alya/ecommerce_serang/utils/FileUtils.kt create mode 100644 app/src/main/res/layout/activity_edit_profile_cust.xml 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +