diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 852a074606a7f04fc1967521dfbc630f783c13df..2227d9b31ffb77ba7037604df029a436ae68e0fd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,8 +98,8 @@ dependencies { // Kotlin components implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20") - api ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") - api ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") + api ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + api ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") //DataBinding diff --git a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt index 0783db7d710e6d76db561377b948c65869b5c4b1..a47dea4cf9aba8010a01534612431e3c9afd6cb6 100644 --- a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt +++ b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt @@ -8,6 +8,7 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix +import android.location.Geocoder import android.os.Bundle import android.os.Handler import android.os.Looper @@ -15,43 +16,56 @@ import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText import android.widget.ImageButton import android.widget.ImageView +import android.widget.RadioGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.exifinterface.media.ExifInterface +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.example.nerbos.R import com.example.nerbos.model.Transaction import com.example.nerbos.model.TransactionCategory import com.example.nerbos.service.Authentication import com.example.nerbos.viewmodel.TransactionViewModel -import java.io.File -import java.io.IOException -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import okhttp3.* +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import okhttp3.Call +import okhttp3.Callback import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.Response import org.json.JSONException import org.json.JSONObject +import java.io.File +import java.io.IOException +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors class ScanFragment : Fragment() { private lateinit var previewView: PreviewView private lateinit var cameraExecutor: ExecutorService private lateinit var imageCapture: ImageCapture + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var geocoder: Geocoder private var fragmentContext: Context? = null private val requestCameraPermissionCode = Manifest.permission.CAMERA @@ -82,6 +96,9 @@ class ScanFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) cameraExecutor = Executors.newSingleThreadExecutor() + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + geocoder = Geocoder(requireContext(), Locale.getDefault()) + if (allPermissionsGranted()) { startCamera() } else { @@ -218,31 +235,71 @@ class ScanFragment : Fragment() { } } - private fun showTransactionTypeDialog(callback: (String) -> Unit) { - val builder = AlertDialog.Builder(requireContext()) - builder.setTitle("Select Transaction Type") - builder.setMessage("Please select the type of transaction you want to add.") - - builder.setPositiveButton("Income") { dialog, _ -> - // Handle income transaction - callback("Income") - dialog.dismiss() + @Suppress("DEPRECATION") + private fun showTransactionInputDialog(callback: (String, String, String) -> Unit) { + val dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.add_scan_transaction, null) + val nameInput = dialogView.findViewById<EditText>(R.id.nameInput) + val locationInput = dialogView.findViewById<EditText>(R.id.locationInput) + val transactionTypeRadioGroup = dialogView.findViewById<RadioGroup>(R.id.transactionTypeRadioGroup) + val mapButton = dialogView.findViewById<ImageButton>(R.id.mapButton) + + transactionTypeRadioGroup.check(R.id.incomeRadioButton) + mapButton.setOnClickListener { + if (ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1) + return@setOnClickListener + } + fusedLocationProviderClient.lastLocation.addOnSuccessListener (requireActivity()){ + location -> + if (location != null) { + val address = geocoder.getFromLocation(location.latitude, location.longitude, 1) + locationInput.setText(address!![0].getAddressLine(0)) + } else { + Toast.makeText(requireContext(), "Failed to get location", Toast.LENGTH_SHORT) + .show() + } + } } - builder.setNegativeButton("Outcome") { dialog, _ -> - // Handle outcome transaction - callback("Outcome") - dialog.dismiss() - } + val dialog = AlertDialog.Builder(requireContext()) + .setTitle("Transaction Input") + .setView(dialogView) + .setPositiveButton("OK") { _, _ -> + val name = nameInput.text.toString() + val location = locationInput.text.toString() + val transactionType = when (transactionTypeRadioGroup.checkedRadioButtonId) { + R.id.incomeRadioButton -> "Income" + R.id.outcomeRadioButton -> "Outcome" + else -> "" + } + if (name.isNotEmpty() && location.isNotEmpty() && transactionType.isNotEmpty()) { + callback(name, location, transactionType) + } else { + // Display error message if any field is empty + Toast.makeText(requireContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show() + // Show the dialog again to allow the user to fill in missing fields + showTransactionInputDialog(callback) + } + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .create() - val dialog = builder.create() dialog.show() dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) } - private fun showTransactionConfirmationDialog(responseBody: String?, selectedTransactionType: String, totalNominal: Float, callback: (Boolean) -> Unit) { + private fun showTransactionConfirmationDialog(responseBody: String?, transactionName: String, selectedTransactionType: String, totalNominal: Float, location: String, callback: (Boolean) -> Unit) { val builder = AlertDialog.Builder(requireContext()) builder.setTitle("Confirm Transaction Details") @@ -264,9 +321,8 @@ class ScanFragment : Fragment() { } } - // Adjust message based on transaction type val transactionTypeMessage = if (selectedTransactionType == "Income") "income" else "outcome" - val message = "$itemDetails\nTotal Nominal: $totalNominal\n\nThis transaction will be added as an $transactionTypeMessage with location: Bandung.\n\nDo you want to proceed?" + val message = "Transaction Name: $transactionName\n\n$itemDetails\nTotal Nominal: $totalNominal\n\nThis transaction will be added as an $transactionTypeMessage with location: $location.\n\nDo you want to proceed?" builder.setMessage(message) builder.setPositiveButton("Yes") { dialog, _ -> @@ -287,7 +343,7 @@ class ScanFragment : Fragment() { } private fun uploadImageToServer(imageFile: File, callback: (Boolean, String?) -> Unit) { - showTransactionTypeDialog { selectedTransactionType -> + showTransactionInputDialog { name, location, transactionType -> val authentication = Authentication(requireContext()) val token = authentication.getToken() val client = OkHttpClient() @@ -309,20 +365,19 @@ class ScanFragment : Fragment() { // Parse the response body to extract transaction items and calculate total nominal val responseBody = response.body?.string() val totalNominal = calculateTotalNominal(responseBody) - val itemDetails = getItemsFromResponse(responseBody) // Run on UI thread to show transaction confirmation dialog requireActivity().runOnUiThread { - showTransactionConfirmationDialog(responseBody, selectedTransactionType, totalNominal ?: 0f) { confirmed -> + showTransactionConfirmationDialog(responseBody, name, transactionType, totalNominal ?: 0f, location) { confirmed -> if (confirmed) { // Create a single transaction and add it to the database totalNominal?.let { nominal -> val transaction = Transaction( userID = authentication.getNim(), - name = "Scan Transaction $itemDetails", - category = if (selectedTransactionType == "Income") TransactionCategory.INCOME else TransactionCategory.OUTCOME, + name = name, + category = if (transactionType == "Income") TransactionCategory.INCOME else TransactionCategory.OUTCOME, nominal = nominal, - location = "Bandung" + location = location ) addTransactionToDatabase(transaction) callback(true, "Transaction added successfully") @@ -347,22 +402,6 @@ class ScanFragment : Fragment() { } } - private fun getItemsFromResponse(responseBody: String?): String { - val itemDetails = StringBuilder() - try { - val jsonObject = JSONObject(responseBody.toString()) - val itemsArray = jsonObject.getJSONObject("items").getJSONArray("items") - for (i in 0 until itemsArray.length()) { - val itemObject = itemsArray.getJSONObject(i) - val name = itemObject.getString("name") - itemDetails.append("$name ") - } - } catch (e: JSONException) { - e.printStackTrace() - } - return itemDetails.toString() - } - private fun calculateTotalNominal(responseBody: String?): Float? { var totalNominal: Float? = null try { diff --git a/app/src/main/res/layout/add_scan_transaction.xml b/app/src/main/res/layout/add_scan_transaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..520807875ceb101da97dbe6549a6fc14b4f58ee0 --- /dev/null +++ b/app/src/main/res/layout/add_scan_transaction.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout 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:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <EditText + android:id="@+id/nameInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/name" + android:inputType="text" + android:autofillHints="Transaction Name" /> + + <RadioGroup + android:id="@+id/transactionTypeRadioGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:orientation="horizontal"> + + <RadioButton + android:id="@+id/incomeRadioButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/income" /> + + <RadioButton + android:id="@+id/outcomeRadioButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/outcome" /> + + </RadioGroup> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="horizontal"> + + <EditText + android:id="@+id/locationInput" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:autofillHints="Location Name" + android:hint="@string/location" + android:inputType="text" + android:layout_marginEnd="20dp" + tools:ignore="NestedWeights" /> + + <ImageButton + android:id="@+id/mapButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/maps" + android:background="@color/transparent" + app:srcCompat="@android:drawable/ic_dialog_map" /> + </LinearLayout> + +</LinearLayout> \ No newline at end of file