From a93d039b27a950c6dd44cd6ef78f5d7e383a3f69 Mon Sep 17 00:00:00 2001 From: shaulascr Date: Tue, 27 May 2025 17:39:51 +0700 Subject: [PATCH] sells update --- app/src/main/AndroidManifest.xml | 13 +- .../data/api/retrofit/ApiService.kt | 7 + .../data/repository/ChatRepository.kt | 149 ++++++++++---- .../ui/auth/RegisterActivity.kt | 2 + .../auth/fragments/RegisterStep3Fragment.kt | 56 ++++++ .../ecommerce_serang/ui/chat/ChatActivity.kt | 184 ++++++++++++------ .../ecommerce_serang/ui/chat/ChatAdapter.kt | 19 +- .../ecommerce_serang/ui/chat/ChatViewModel.kt | 18 +- .../ui/notif/PersonalNotificationAdapter.kt | 11 +- app/src/main/res/layout/activity_chat.xml | 25 +-- app/src/main/res/layout/activity_checkout.xml | 1 + .../layout/activity_detail_order_status.xml | 1 + .../res/layout/activity_detail_payment.xml | 1 + .../res/layout/activity_detail_profile.xml | 1 + .../res/layout/activity_edit_profile_cust.xml | 1 + .../main/res/layout/activity_notification.xml | 30 +-- app/src/main/res/layout/activity_payment.xml | 1 + .../main/res/layout/activity_payment_info.xml | 1 + app/src/main/res/layout/activity_register.xml | 1 + .../res/layout/activity_register_store.xml | 5 +- app/src/main/res/values/themes.xml | 3 + 21 files changed, 390 insertions(+), 140 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31e1666..4edca1b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:exported="false" /> + + + android:exported="false" + android:windowSoftInputMode="adjustResize|stateHidden" /> @@ -88,6 +97,7 @@ android:exported="false" /> 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 57e4742..099f70f 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 @@ -450,6 +450,13 @@ interface ApiService { @Part chatimg: MultipartBody.Part? ): Response + @Multipart + @POST("sendchat") + suspend fun sendChatMessage( + @PartMap parts: Map, + @Part chatimg: MultipartBody.Part? = null + ): Response + @PUT("chatstatus") suspend fun updateChatStatus( @Body request: UpdateChatRequest diff --git a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt index 68c0a94..ccfb887 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/data/repository/ChatRepository.kt @@ -8,8 +8,9 @@ import com.alya.ecommerce_serang.data.api.response.chat.ChatItemList import com.alya.ecommerce_serang.data.api.response.chat.SendChatResponse import com.alya.ecommerce_serang.data.api.response.chat.UpdateChatResponse import com.alya.ecommerce_serang.data.api.retrofit.ApiService -import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File @@ -38,62 +39,130 @@ class ChatRepository @Inject constructor( suspend fun sendChatMessage( storeId: Int, message: String, - productId: Int? = null, - imageFile: File? = null, - chatRoomId: Int? = null // Not used in the actual API call but kept for compatibility + productId: Int?, // Nullable and optional + imageFile: File? = null // Nullable and optional ): Result { return try { - // Create multipart request parts - val storeIdPart = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) - val messagePart = message.toRequestBody("text/plain".toMediaTypeOrNull()) + val parts = mutableMapOf() - // Add product ID part if provided - val productIdPart = if (productId != null && productId > 0) { - productId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) - } else { - null + // Required fields + parts["store_id"] = storeId.toString().toRequestBody("text/plain".toMediaType()) + parts["message"] = message.toRequestBody("text/plain".toMediaType()) + + // Optional: Only include if productId is valid + if (productId != null && productId > 0) { + parts["product_id"] = productId.toString().toRequestBody("text/plain".toMediaType()) } - // Create image part if file is provided - val imagePart = if (imageFile != null && imageFile.exists()) { - val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull()) - MultipartBody.Part.createFormData("chatimg", imageFile.name, requestFile) - } else { - null + // Optional: Only include if imageFile is valid + val imagePart = imageFile?.takeIf { it.exists() }?.let { file -> +// val requestFile = file.asRequestBody("image/*".toMediaType()) + val mimeType = when { + file.name.endsWith(".png", ignoreCase = true) -> "image/png" + file.name.endsWith(".jpg", ignoreCase = true) || file.name.endsWith(".jpeg", ignoreCase = true) -> "image/jpeg" + else -> "image/jpeg" // fallback + } + val requestFile = file.asRequestBody(mimeType.toMediaType()) + MultipartBody.Part.createFormData("chatimg", file.name, requestFile) } - // Debug log the request parameters - Log.d("ChatRepository", "Sending chat with: storeId=$storeId, productId=$productId, " + - "message length=${message.length}, hasImage=${imageFile != null}") + // Log the parts map keys and values (string representations) + Log.d("ChatRepository", "Sending chat message with parts:") + parts.forEach { (key, body) -> + Log.d("ChatRepository", "Key: $key, Value (approx): ${bodyToString(body)}") + } + Log.d("ChatRepository", "Sending chat message with imagePart: ${imagePart != null}") - // Make API call using your actual endpoint and parameter names - val response = apiService.sendChatLine( - storeId = storeIdPart, - message = messagePart, - productId = productIdPart, - chatimg = imagePart - ) - - Log.d("ChatRepository", "check data productId=$productIdPart, storeId=$storeIdPart, messageTxt=$messagePart, chatImg=$imagePart") + // Send request + val response = apiService.sendChatMessage(parts, imagePart) if (response.isSuccessful) { - val body = response.body() - if (body != null) { - Result.Success(body) - } else { - Result.Error(Exception("Empty response body")) - } + response.body()?.let { Result.Success(it) } ?: Result.Error(Exception("Empty response body")) } else { - val errorBody = response.errorBody()?.string() ?: "{}" - Log.e("ChatRepository", "API Error: ${response.code()} - $errorBody") - Result.Error(Exception("API Error: ${response.code()} - $errorBody")) + val errorMsg = response.errorBody()?.string().orEmpty() + Log.e("ChatRepository", "API Error: ${response.code()} - $errorMsg") + Result.Error(Exception("API Error: ${response.code()} - $errorMsg")) } + } catch (e: Exception) { - Log.e("ChatRepository", "Exception sending message", e) + Log.e("ChatRepository", "Exception sending chat message", e) Result.Error(e) } } + // Helper function to get string content from RequestBody (best effort) + private fun bodyToString(requestBody: RequestBody): String { + return try { + val buffer = okio.Buffer() + requestBody.writeTo(buffer) + buffer.readUtf8() + } catch (e: Exception) { + "Could not read body" + } + } + +// suspend fun sendChatMessage( +// storeId: Int, +// message: String, +// productId: Int?, +// imageFile: File? = null, +// chatRoomId: Int? = null +// ): Result { +// return try { +// Log.d(TAG, "=== SEND CHAT MESSAGE ===") +// Log.d(TAG, "StoreId: $storeId") +// Log.d(TAG, "Message: '$message'") +// Log.d(TAG, "ProductId: $productId") +// Log.d(TAG, "ImageFile: ${imageFile?.absolutePath}") +// Log.d(TAG, "ImageFile exists: ${imageFile?.exists()}") +// Log.d(TAG, "ImageFile size: ${imageFile?.length()} bytes") +// +// // Convert primitive fields to RequestBody +// val storeIdBody = storeId.toString().toRequestBody("text/plain".toMediaTypeOrNull()) +// val messageBody = message.toRequestBody("text/plain".toMediaTypeOrNull()) +// val productIdBody = productId?.takeIf { it > 0 } // null if 0 +// ?.toString() +// ?.toRequestBody("text/plain".toMediaTypeOrNull()) +// +// // Convert image file to MultipartBody.Part if exists +// val imagePart: MultipartBody.Part? = imageFile?.takeIf { it.exists() }?.let { file -> +// val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) +// MultipartBody.Part.createFormData("chatimg", file.name, requestFile) +// } +// +// +// +// // Call the API +// Log.d(TAG, "Sending request. ProductIdBody is null: ${productIdBody == null}") +// +// val response = apiService.sendChatLine( +// storeId = storeIdBody, +// message = messageBody, +// productId = productIdBody, +// chatimg = imagePart +// ) +// +// // Handle API response +// if (response.isSuccessful) { +// response.body()?.let { +// Log.d(TAG, "Success: ${it.message}") +// Result.Success(it) +// } ?: run { +// Log.e(TAG, "Response body is null") +// Result.Error(Exception("Empty response body")) +// } +// } else { +// val errorMsg = response.errorBody()?.string() ?: "Unknown error" +// Log.e(TAG, "API Error: ${response.code()} - $errorMsg") +// Result.Error(Exception("API Error: ${response.code()} - $errorMsg")) +// } +// +// } catch (e: Exception) { +// Log.e(TAG, "Exception sending chat message", e) +// Result.Error(e) +// } +// } + suspend fun updateMessageStatus( messageId: Int, status: String diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt index c8f6585..04b5780 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/RegisterActivity.kt @@ -59,6 +59,8 @@ class RegisterActivity : AppCompatActivity() { windowInsets } + + Log.d("RegisterActivity", "Token in storage: '${sessionManager.getToken()}'") Log.d("RegisterActivity", "User ID in storage: '${sessionManager.getUserId()}'") diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt index ec34458..e73281e 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/auth/fragments/RegisterStep3Fragment.kt @@ -8,6 +8,9 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.alya.ecommerce_serang.R @@ -91,6 +94,8 @@ class RegisterStep3Fragment : Fragment() { // Set up province and city dropdowns setupAutoComplete() + setupEdgeToEdge() + // Set up button listeners binding.btnPrevious.setOnClickListener { // Go back to the previous step @@ -116,6 +121,55 @@ class RegisterStep3Fragment : Fragment() { setupCityObserver() } + private fun setupEdgeToEdge() { + // Apply insets to your fragment's root view + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets -> + val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + systemBars.left, + systemBars.top, + systemBars.right, + systemBars.bottom + ) + windowInsets + } + + // Set up IME animation callback + ViewCompat.setWindowInsetsAnimationCallback( + binding.root, + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + var startBottom = 0f + var endBottom = 0f + + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + startBottom = binding.root.bottom.toFloat() + } + + override fun onStart( + animation: WindowInsetsAnimationCompat, + bounds: WindowInsetsAnimationCompat.BoundsCompat + ): WindowInsetsAnimationCompat.BoundsCompat { + endBottom = binding.root.bottom.toFloat() + return bounds + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList + ): WindowInsetsCompat { + val imeAnimation = runningAnimations.find { + it.typeMask and WindowInsetsCompat.Type.ime() != 0 + } ?: return insets + + binding.root.translationY = + (startBottom - endBottom) * (1 - imeAnimation.interpolatedFraction) + + return insets + } + } + ) + } + private fun setupAutoComplete() { // Same implementation as before binding.autoCompleteProvinsi.setAdapter(provinceAdapter) @@ -351,6 +405,8 @@ class RegisterStep3Fragment : Fragment() { override fun onDestroyView() { super.onDestroyView() + ViewCompat.setOnApplyWindowInsetsListener(binding.root, null) + ViewCompat.setWindowInsetsAnimationCallback(binding.root, null) _binding = null } // diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt index 453c9b2..d9cc397 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatActivity.kt @@ -3,6 +3,7 @@ package com.alya.ecommerce_serang.ui.chat import android.Manifest import android.app.Activity import android.app.AlertDialog +import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -12,6 +13,7 @@ import android.text.Editable import android.text.TextWatcher import android.util.Log import android.view.View +import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts @@ -22,6 +24,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager @@ -40,6 +43,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import javax.inject.Inject +import kotlin.math.max @AndroidEntryPoint class ChatActivity : AppCompatActivity() { @@ -100,16 +104,7 @@ class ChatActivity : AppCompatActivity() { 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 - } + // Get parameters from intent val storeId = intent.getIntExtra(Constants.EXTRA_STORE_ID, 0) @@ -146,6 +141,84 @@ class ChatActivity : AppCompatActivity() { .placeholder(R.drawable.placeholder_image) .into(binding.imgProfile) + ViewCompat.setOnApplyWindowInsetsListener(binding.layoutChatInput) { view, insets -> + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) + val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + + val bottomPadding = max(imeInsets.bottom, navBarInsets.bottom) + view.setPadding(view.paddingLeft, view.paddingTop, view.paddingRight, bottomPadding) + insets + } + +// Handle top inset on toolbar (status bar height) + ViewCompat.setOnApplyWindowInsetsListener(binding.chatToolbar) { view, insets -> + val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top + view.setPadding(view.paddingLeft, statusBarHeight, view.paddingRight, view.paddingBottom) + insets + } + + ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerChat) { view, insets -> + val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val bottomPadding = binding.layoutChatInput.height + navBarInsets.bottom + + view.setPadding( + view.paddingLeft, + view.paddingTop, + view.paddingRight, + bottomPadding + ) + insets + } + +// For RecyclerView, add bottom padding = chat input height + nav bar height (to avoid last message hidden) + + ViewCompat.setWindowInsetsAnimationCallback(binding.root, + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + + private var startPaddingBottom = 0 + private var endPaddingBottom = 0 + + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + startPaddingBottom = binding.layoutChatInput.paddingBottom + } + + override fun onStart( + animation: WindowInsetsAnimationCompat, + bounds: WindowInsetsAnimationCompat.BoundsCompat + ): WindowInsetsAnimationCompat.BoundsCompat { + endPaddingBottom = binding.layoutChatInput.paddingBottom + return bounds + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: MutableList + ): WindowInsetsCompat { + val imeAnimation = runningAnimations.find { + it.typeMask and WindowInsetsCompat.Type.ime() != 0 + } ?: return insets + + val animatedBottomPadding = startPaddingBottom + + (endPaddingBottom - startPaddingBottom) * imeAnimation.interpolatedFraction + + binding.layoutChatInput.setPadding( + binding.layoutChatInput.paddingLeft, + binding.layoutChatInput.paddingTop, + binding.layoutChatInput.paddingRight, + animatedBottomPadding.toInt() + ) + + binding.recyclerChat.setPadding( + binding.recyclerChat.paddingLeft, + binding.recyclerChat.paddingTop, + binding.recyclerChat.paddingRight, + animatedBottomPadding.toInt() + binding.layoutChatInput.height + ) + + return insets + } + }) + // Set chat parameters to ViewModel viewModel.setChatParameters( storeId = storeId, @@ -178,6 +251,12 @@ class ChatActivity : AppCompatActivity() { stackFromEnd = true } } +// binding.recyclerChat.setPadding( +// binding.recyclerChat.paddingLeft, +// binding.recyclerChat.paddingTop, +// binding.recyclerChat.paddingRight, +// binding.layoutChatInput.height + binding.root.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0 +// ) } @@ -222,6 +301,11 @@ class ChatActivity : AppCompatActivity() { override fun afterTextChanged(s: Editable?) {} }) + + binding.editTextMessage.requestFocus() + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(binding.editTextMessage, InputMethodManager.SHOW_IMPLICIT) + } private fun observeViewModel() { @@ -256,10 +340,17 @@ class ChatActivity : AppCompatActivity() { binding.tvSellerName.text = state.storeName binding.tvStoreName.text=state.storeName + val fullImageUrl = when (val img = state.productImageUrl) { + is String -> { + if (img.startsWith("/")) BASE_URL + img.substring(1) else img + } + else -> R.drawable.placeholder_image + } + // Load product image if (!state.productImageUrl.isNullOrEmpty()) { Glide.with(this@ChatActivity) - .load(BASE_URL + state.productImageUrl) + .load(fullImageUrl) .centerCrop() .placeholder(R.drawable.placeholder_image) .error(R.drawable.placeholder_image) @@ -294,8 +385,6 @@ class ChatActivity : AppCompatActivity() { }) } - - private fun showOptionsMenu() { val options = arrayOf( getString(R.string.block_user), @@ -380,67 +469,36 @@ class ChatActivity : AppCompatActivity() { try { Log.d(TAG, "Processing selected image: $uri") - // First try the direct approach to get the file path - var filePath: String? = null + // Always use the copy-to-cache approach for reliability + contentResolver.openInputStream(uri)?.use { inputStream -> + val fileName = "chat_img_${System.currentTimeMillis()}.jpg" + val outputFile = File(cacheDir, fileName) - // For newer Android versions, we need to handle content URIs properly - if (uri.scheme == "content") { - val cursor = contentResolver.query(uri, null, null, null, null) - cursor?.use { - if (it.moveToFirst()) { - val columnIndex = it.getColumnIndex(MediaStore.Images.Media.DATA) - if (columnIndex != -1) { - filePath = it.getString(columnIndex) - Log.d(TAG, "Found file path from cursor: $filePath") - } - } + outputFile.outputStream().use { outputStream -> + inputStream.copyTo(outputStream) } - // If we couldn't get the path directly, create a copy in our cache directory - if (filePath == null) { - contentResolver.openInputStream(uri)?.use { inputStream -> - val fileName = "img_${System.currentTimeMillis()}.jpg" - val outputFile = File(cacheDir, fileName) - - outputFile.outputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - - filePath = outputFile.absolutePath - Log.d(TAG, "Created temp file from input stream: $filePath") - } - } - } else if (uri.scheme == "file") { - // Direct file URI - filePath = uri.path - Log.d(TAG, "Got file path directly from URI: $filePath") - } - - // Process the file path - if (filePath != null) { - val file = File(filePath) - if (file.exists()) { - // Check file size (limit to 5MB) - if (file.length() > 5 * 1024 * 1024) { - Toast.makeText(this, "Image too large (max 5MB), please select a smaller image", Toast.LENGTH_SHORT).show() + if (outputFile.exists() && outputFile.length() > 0) { + if (outputFile.length() > 5 * 1024 * 1024) { + Log.e(TAG, "File too large: ${outputFile.length()} bytes") + Toast.makeText(this, "Image too large (max 5MB)", Toast.LENGTH_SHORT).show() return } - // Set the file to the ViewModel - viewModel.setSelectedImageFile(file) - Toast.makeText(this, R.string.image_selected, Toast.LENGTH_SHORT).show() - Log.d(TAG, "Successfully set image file: ${file.absolutePath}, size: ${file.length()} bytes") + Log.d(TAG, "Image processed successfully: ${outputFile.absolutePath}, size: ${outputFile.length()}") + viewModel.setSelectedImageFile(outputFile) + Toast.makeText(this, "Image selected", Toast.LENGTH_SHORT).show() } else { - Log.e(TAG, "File does not exist: $filePath") - Toast.makeText(this, "Could not access the selected image", Toast.LENGTH_SHORT).show() + Log.e(TAG, "Failed to create image file") + Toast.makeText(this, "Failed to process image", Toast.LENGTH_SHORT).show() } - } else { - Log.e(TAG, "Could not get file path from URI: $uri") - Toast.makeText(this, "Could not process the selected image", Toast.LENGTH_SHORT).show() + } ?: run { + Log.e(TAG, "Could not open input stream for URI: $uri") + Toast.makeText(this, "Could not access image", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { Log.e(TAG, "Error handling selected image", e) - Toast.makeText(this, "Error processing image: ${e.message}", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt index 0e2f083..484803f 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatAdapter.kt @@ -78,8 +78,16 @@ class ChatAdapter : ListAdapter(ChatMess // Handle attachment if exists if (message.attachment?.isNotEmpty() == true) { binding.imgAttachment.visibility = View.VISIBLE + + val fullImageUrl = when (val img = message.attachment) { + is String -> { + if (img.startsWith("/")) BASE_URL + img.substring(1) else img + } + else -> R.drawable.placeholder_image + } + Glide.with(binding.root.context) - .load(BASE_URL + message.attachment) + .load(fullImageUrl) .centerCrop() .placeholder(R.drawable.placeholder_image) .error(R.drawable.placeholder_image) @@ -101,10 +109,17 @@ class ChatAdapter : ListAdapter(ChatMess binding.tvTimestamp.text = message.time // Handle attachment if exists + val fullImageUrl = when (val img = message.attachment) { + is String -> { + if (img.startsWith("/")) BASE_URL + img.substring(1) else img + } + else -> R.drawable.placeholder_image + } + if (message.attachment?.isNotEmpty() == true) { binding.imgAttachment.visibility = View.VISIBLE Glide.with(binding.root.context) - .load(BASE_URL + message.attachment) + .load(fullImageUrl) .centerCrop() .placeholder(R.drawable.placeholder_image) .error(R.drawable.placeholder_image) diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt index 989dedd..1f15130 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/chat/ChatViewModel.kt @@ -46,7 +46,7 @@ class ChatViewModel @Inject constructor( // Store and product parameters private var storeId: Int = 0 - private var productId: Int? = 0 + private var productId: Int = 0 private var currentUserId: Int? = null private var defaultUserId: Int = 0 @@ -100,8 +100,9 @@ class ChatViewModel @Inject constructor( productRating: Float? = 0f, storeName: String ) { + this.productId = if (productId != null && productId > 0) productId else 0 + this.storeId = storeId - this.productId = productId!! this.productName = productName.toString() this.productPrice = productPrice.toString() this.productImage = productImage.toString() @@ -247,6 +248,11 @@ class ChatViewModel @Inject constructor( * Sends a chat message */ fun sendMessage(message: String) { + Log.d(TAG, "=== SEND MESSAGE ===") + Log.d(TAG, "Message: '$message'") + Log.d(TAG, "Has attachment: ${selectedImageFile != null}") + Log.d(TAG, "Selected image file: ${selectedImageFile?.absolutePath}") + Log.d(TAG, "File exists: ${selectedImageFile?.exists()}") if (message.isBlank() && selectedImageFile == null) { Log.e(TAG, "Cannot send message: Both message and image are empty") return @@ -282,12 +288,14 @@ class ChatViewModel @Inject constructor( // Send the message using the repository // Note: We keep the chatRoomId parameter for compatibility with the repository method signature, // but it's not actually used in the API call + val safeProductId = if (productId == 0) null else productId + + val result = chatRepository.sendChatMessage( storeId = storeId, message = message, - productId = productId, - imageFile = selectedImageFile, - chatRoomId = existingChatRoomId + productId = safeProductId, + imageFile = selectedImageFile ) when (result) { diff --git a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/PersonalNotificationAdapter.kt b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/PersonalNotificationAdapter.kt index e60aa1c..cafb9e5 100644 --- a/app/src/main/java/com/alya/ecommerce_serang/ui/notif/PersonalNotificationAdapter.kt +++ b/app/src/main/java/com/alya/ecommerce_serang/ui/notif/PersonalNotificationAdapter.kt @@ -44,7 +44,14 @@ class PersonalNotificationAdapter( fun bind(notification: NotifItem) { binding.apply { - tvNotificationType.text = notification.type + val typeNotif = notification.type.toString() + if(typeNotif == "User"){ + tvNotificationType.text = "Pembelian" + } else if (typeNotif == "Store"){ + tvNotificationType.text = "Penjualan" + } else { + tvNotificationType.text = notification.type + } tvTitle.text = notification.title tvDescription.text = notification.message @@ -63,7 +70,7 @@ class PersonalNotificationAdapter( try { // Parse the date with the expected format from API val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) - val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + val outputFormat = SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault()) val date = inputFormat.parse(createdAt) date?.let { diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 902de35..d60c39e 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="false" android:theme="@style/Theme.Ecommerce_serang" tools:context=".ui.chat.ChatActivity"> @@ -55,17 +56,17 @@ app:layout_constraintTop_toTopOf="@+id/imgProfile" app:layout_constraintEnd_toStartOf="@+id/btnOptions" /> - + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_checkout.xml b/app/src/main/res/layout/activity_checkout.xml index ba30bd8..8c793d4 100644 --- a/app/src/main/res/layout/activity_checkout.xml +++ b/app/src/main/res/layout/activity_checkout.xml @@ -7,6 +7,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black_800" + android:theme="@style/Theme.Ecommerce_serang" tools:context=".ui.order.CheckoutActivity"> + android:fontFamily="@font/dmsans_semibold" /> - + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/tabLayout" /> diff --git a/app/src/main/res/layout/activity_register_store.xml b/app/src/main/res/layout/activity_register_store.xml index 36f8916..2ac26d7 100644 --- a/app/src/main/res/layout/activity_register_store.xml +++ b/app/src/main/res/layout/activity_register_store.xml @@ -21,7 +21,7 @@ android:text="Buka Toko" android:textColor="@android:color/white" android:textSize="20sp" - android:textStyle="bold" /> + android:fontFamily="@font/dmsans_medium" /> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 24782af..adeeaac 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -40,6 +40,9 @@ @android:color/transparent @android:color/transparent + + @style/body_medium +