diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c661268957eb56058df4c347bcb3edc02b4c05b6..4efe4428e03b11a08b97f6e7e4cdb673a9813256 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("kotlin-kapt") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } android { @@ -47,8 +48,17 @@ dependencies { implementation("com.android.volley:volley:1.2.1") implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("com.google.android.gms:play-services-maps:18.2.0") + implementation("com.google.android.gms:play-services-location:21.2.0") + implementation("androidx.camera:camera-core:1.3.2") + implementation("androidx.camera:camera-camera2:1.3.2") + implementation("androidx.camera:camera-lifecycle:1.3.2") + implementation("androidx.camera:camera-view:1.3.2") + implementation("androidx.exifinterface:exifinterface:1.3.7") + implementation("com.squareup.okhttp3:okhttp:4.9.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -88,9 +98,12 @@ 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") + // POI: excel + implementation ("org.apache.poi:poi:5.0.0") + implementation ("org.apache.poi:poi-ooxml:5.2.3") //DataBinding kapt ("com.android.databinding:compiler:3.2.0-alpha10") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e52baae0b52c0d7b23950ee6472a45ee56941c00..221bdfd8e26f2f80a3e88bbde29647db4fcc099f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> - + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-feature android:name="android.hardware.camera.any" /> + <application android:allowBackup="true" android:icon="@mipmap/ic_primary_logo" @@ -12,6 +17,25 @@ android:supportsRtl="true" android:theme="@style/Theme.NerbOS"> + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.provider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths" /> + </provider> + + <meta-data + android:name="com.google.android.geo.API_KEY" + android:value="AIzaSyBdVOQUHGOg7TS74kPCzoEtDDlGGZHpAEs" /> + + <activity + android:name=".MapsActivity" + android:exported="false" + android:label="@string/title_activity_maps" /> + <service android:name=".service.NetworkManagerService" /> <service diff --git a/app/src/main/java/com/example/nerbos/MapsActivity.kt b/app/src/main/java/com/example/nerbos/MapsActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4ccc07f8f2b97f2bbf2647fb5aab32968e20445 --- /dev/null +++ b/app/src/main/java/com/example/nerbos/MapsActivity.kt @@ -0,0 +1,73 @@ +package com.example.nerbos + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Camera +import android.location.Address +import android.location.Geocoder +import android.location.Location +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.app.ActivityCompat + +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions +import com.example.nerbos.databinding.ActivityMapsBinding +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import com.google.android.gms.maps.model.Marker +import java.util.Locale + +class MapsActivity : AppCompatActivity(), OnMapReadyCallback { + + private lateinit var mMap: GoogleMap + private lateinit var binding: ActivityMapsBinding + private lateinit var location: Address + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityMapsBinding.inflate(layoutInflater) + setContentView(binding.root) + + // Obtain the SupportMapFragment and get notified when the map is ready to be used. + val mapFragment = supportFragmentManager + .findFragmentById(R.id.map) as SupportMapFragment + mapFragment.getMapAsync(this) + + val myIntent = intent + val locationName : String? = myIntent.getStringExtra("locationName") + val geocoder = Geocoder(this, Locale.getDefault()) + val address = geocoder.getFromLocationName(locationName!!, 1) + if (address != null) { + location = address[0] + } + + // Navigation + findViewById<ImageView>(R.id.backButton).setOnClickListener { + val mainIntent: Intent = Intent(this, MainActivity::class.java) + startActivity(mainIntent) + finish() + } + + } + + override fun onMapReady(googleMap: GoogleMap) { + mMap = googleMap + mMap.uiSettings.isZoomControlsEnabled = true + val currentLatLong : LatLng = LatLng(location.latitude,location.longitude) + val markerOptions = MarkerOptions().position(currentLatLong).title("Current Location") + mMap.addMarker(markerOptions) + mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLatLong, 15f)) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/nerbos/data/TransactionDao.kt b/app/src/main/java/com/example/nerbos/data/TransactionDao.kt index ce1a9db3b911612993363e6d50492612ee2c2712..29683aee64443d98d4a9bfd898ca249449efe82b 100644 --- a/app/src/main/java/com/example/nerbos/data/TransactionDao.kt +++ b/app/src/main/java/com/example/nerbos/data/TransactionDao.kt @@ -8,6 +8,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update import com.example.nerbos.model.Transaction +import com.example.nerbos.model.TransactionCategory @Dao interface TransactionDao { @@ -22,4 +23,8 @@ interface TransactionDao { @Delete suspend fun deleteTransation(transaction: Transaction) + + @Query("SELECT SUM(nominal) FROM transaction_table WHERE userID = :userID AND category = :category") + fun getSum(userID: Int, category: TransactionCategory): LiveData<Float> + } \ No newline at end of file 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 7161de166dfce509711fa63ec72839a9684b2105..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 @@ -1,33 +1,86 @@ package com.example.nerbos.fragments.scan +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +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 androidx.fragment.app.Fragment +import android.os.Handler +import android.os.Looper +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.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.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 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 -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [ScanFragment.newInstance] factory method to - * create an instance of this fragment. - */ class ScanFragment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } + 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 + private val uploadURL: String by lazy { + requireContext().getString(R.string.backend_api_scan) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + fragmentContext = context + } + + override fun onDetach() { + super.onDetach() + fragmentContext = null } override fun onCreateView( @@ -35,26 +88,370 @@ class ScanFragment : Fragment() { savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_scan, container, false) - } - - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment Scan. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - ScanFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) + val view = inflater.inflate(R.layout.fragment_scan, container, false) + previewView = view.findViewById(R.id.previewView) + return view + } + + 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 { + requestCameraPermission.launch(requestCameraPermissionCode) + } + + view.findViewById<ImageButton>(R.id.captureButton).setOnClickListener { + takePicture() + } + + view.findViewById<ImageButton>(R.id.galleryButton).setOnClickListener { + dispatchGalleryIntent() + } + } + + private fun allPermissionsGranted() = ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + imageCapture = ImageCapture.Builder().build() + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) + } catch (e: Exception) { + e.printStackTrace() + } + + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun takePicture() { + val imageCapture = imageCapture + + // Create output file to hold the captured image + val externalFilesDirs = requireContext().getExternalFilesDirs(null) + val photoFile = File(externalFilesDirs.firstOrNull(), "${System.currentTimeMillis()}.jpg") + + // Setup image capture metadata + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + // Capture the image + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Toast.makeText(requireContext(), "Error capturing image: ${exc.message}", Toast.LENGTH_SHORT).show() + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + // Image captured successfully, read orientation metadata + val savedUri = photoFile.toUri() + val imageBitmap = BitmapFactory.decodeFile(savedUri.path) + val rotatedBitmap = savedUri.path?.let { + rotateImageIfRequired(imageBitmap, + it + ) + } + showImagePopup(rotatedBitmap) + } + }) + } + + private fun rotateImageIfRequired(bitmap: Bitmap, path: String): Bitmap { + val ei = ExifInterface(path) + val orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + + return when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f) + ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f) + ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f) + else -> bitmap + } + } + + private fun rotateImage(source: Bitmap, angle: Float): Bitmap { + val matrix = Matrix() + matrix.postRotate(angle) + return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true) + } + + private fun dispatchGalleryIntent() { + val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) + requestGallery.launch(galleryIntent) + } + + private fun showImagePopup(imageBitmap: Bitmap?) { + if (imageBitmap != null) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Use this picture?") + .setPositiveButton("Yes") { dialog, _ -> + val imageFile = File.createTempFile("image", ".jpg", requireContext().cacheDir) + imageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageFile.outputStream()) + uploadImageToServer(imageFile) { success, message -> + if (success) { + showToastOnUIThread(message.toString()) + } else { + showToastOnUIThread(message.toString()) + } + } + dialog.dismiss() + } + .setNegativeButton("No") { dialog, _ -> + showToastOnUIThread("Scan cancelled") + dialog.dismiss() + } + .setView(ImageView(requireContext()).apply { + setImageBitmap(imageBitmap) + adjustViewBounds = true + }) + .show() + .apply { + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + } + } else { + Toast.makeText(requireContext(), "Failed to load image", Toast.LENGTH_SHORT).show() + } + } + + @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() + } + } + } + + 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() + + 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?, transactionName: String, selectedTransactionType: String, totalNominal: Float, location: String, callback: (Boolean) -> Unit) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Confirm Transaction Details") + + // Parse the JSON response body to extract item details + val itemDetails = StringBuilder() + calculateTotalNominal(responseBody)?.let { + 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") + val qty = itemObject.getInt("qty") + val price = itemObject.getDouble("price").toFloat() + itemDetails.append("$name: $qty x $price\n") } + } catch (e: JSONException) { + e.printStackTrace() } + } + + val transactionTypeMessage = if (selectedTransactionType == "Income") "income" else "outcome" + 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, _ -> + callback(true) + dialog.dismiss() + } + + builder.setNegativeButton("No") { dialog, _ -> + callback(false) + dialog.dismiss() + } + + 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 uploadImageToServer(imageFile: File, callback: (Boolean, String?) -> Unit) { + showTransactionInputDialog { name, location, transactionType -> + val authentication = Authentication(requireContext()) + val token = authentication.getToken() + val client = OkHttpClient() + + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", imageFile.name, imageFile.asRequestBody("image/*".toMediaTypeOrNull())) + .build() + + val request = Request.Builder() + .url(uploadURL) + .header("Authorization", "Bearer $token") + .post(requestBody) + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + // Parse the response body to extract transaction items and calculate total nominal + val responseBody = response.body?.string() + val totalNominal = calculateTotalNominal(responseBody) + + // Run on UI thread to show transaction confirmation dialog + requireActivity().runOnUiThread { + 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 = name, + category = if (transactionType == "Income") TransactionCategory.INCOME else TransactionCategory.OUTCOME, + nominal = nominal, + location = location + ) + addTransactionToDatabase(transaction) + callback(true, "Transaction added successfully") + } ?: run { + callback(false, "Failed to calculate total nominal") + } + } else { + callback(false, "Transaction cancelled") + } + } + } + } else { + callback(false, "Failed to upload image. Please try again later.") + } + } + + override fun onFailure(call: Call, e: IOException) { + showToastOnUIThread("Failed to upload image: ${e.message}") + callback(false, e.message) + } + }) + } + } + + private fun calculateTotalNominal(responseBody: String?): Float? { + var totalNominal: Float? = null + try { + val jsonObject = JSONObject(responseBody.toString()) + val itemsArray = jsonObject.getJSONObject("items").getJSONArray("items") + totalNominal = 0f + for (i in 0 until itemsArray.length()) { + val itemObject = itemsArray.getJSONObject(i) + val qty = itemObject.getInt("qty") + val price = itemObject.getDouble("price").toFloat() + totalNominal += qty * price + } + } catch (e: JSONException) { + e.printStackTrace() + } + return totalNominal + } + + private fun addTransactionToDatabase(transaction: Transaction) { + val transactionViewModel = ViewModelProvider(requireActivity())[TransactionViewModel::class.java] + transactionViewModel.addTransaction(transaction) + } + + private fun showToastOnUIThread(message: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + } + + private val requestCameraPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + startCamera() + } else { + // Handle the case where the user denied the permission + Toast.makeText(requireContext(), "Camera permission denied", Toast.LENGTH_SHORT).show() + } + } + + private val requestGallery = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val imageUri = result.data?.data + val inputStream = requireContext().contentResolver.openInputStream(imageUri!!) + val imageBitmap = BitmapFactory.decodeStream(inputStream) + showImagePopup(imageBitmap) + } + } + + override fun onDestroy() { + super.onDestroy() + cameraExecutor.shutdown() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/nerbos/fragments/statistic/StatisticFragment.kt b/app/src/main/java/com/example/nerbos/fragments/statistic/StatisticFragment.kt index 55768bf7c265c09097eb8f4c1c0ad305f85daab6..df203cb4033c734c10f9e352516adedef5014e70 100644 --- a/app/src/main/java/com/example/nerbos/fragments/statistic/StatisticFragment.kt +++ b/app/src/main/java/com/example/nerbos/fragments/statistic/StatisticFragment.kt @@ -1,60 +1,182 @@ package com.example.nerbos.fragments.statistic +import android.graphics.Color +import android.graphics.Typeface import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.example.nerbos.R +import com.example.nerbos.databinding.FragmentStatisticBinding +import com.example.nerbos.model.TransactionCategory +import com.example.nerbos.viewmodel.TransactionViewModel +import com.example.nerbos.service.Authentication +import com.github.mikephil.charting.animation.Easing +import com.github.mikephil.charting.charts.PieChart +import com.github.mikephil.charting.data.PieData +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.data.PieEntry +import com.github.mikephil.charting.formatter.PercentFormatter +import com.github.mikephil.charting.utils.MPPointF +import kotlinx.coroutines.launch +import java.text.NumberFormat +import java.util.Locale -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [StatisticFragment.newInstance] factory method to - * create an instance of this fragment. - */ class StatisticFragment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null + + private lateinit var transactionViewModel: TransactionViewModel + private lateinit var authentication: Authentication + private lateinit var pieChart: PieChart + private lateinit var binding: FragmentStatisticBinding + private val liveDataReady = MutableLiveData<Boolean>() + private var sumIncome: Float = 0.0f + private var sumOutcome : Float = 0.0f override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) + } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_statistic, container, false) + binding = FragmentStatisticBinding.inflate(inflater, container, false) + pieChart = binding.pieChart + + // Create an authentication object + authentication = Authentication(requireContext()) + // Set the view model for the transaction and set the user id + transactionViewModel = ViewModelProvider(this)[TransactionViewModel::class.java] + + // Get the data from the database + getData() + + // Set up the pie chart + pieChartSetup() + + // Display the pie chart + displayPieChart() + + return binding.root } - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment Statistic. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - StatisticFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) + private fun getData() { + lifecycleScope.launch { + // Get the sum of income and outcome + val nim = authentication.getNim() + val income = transactionViewModel.getSums(nim, TransactionCategory.INCOME) + val outcome = transactionViewModel.getSums(nim, TransactionCategory.OUTCOME) + + // Use MediatorLiveData to observe both income and outcome + val result = MediatorLiveData<Float>().apply { + addSource(income) { value -> setValue(value) } + addSource(outcome) { value -> setValue(value) } + } + + // Observing the result + result.observe(viewLifecycleOwner) { + if (income.value != null && outcome.value != null) { + sumIncome = income.value!! + sumOutcome = outcome.value!! + liveDataReady.postValue(true) } } + } } + + // Display a pie chart of the income and outcome + private fun pieChartSetup() { + // Set the pie chart: percentage, description, extra offsets + pieChart.setUsePercentValues(true) + pieChart.description.isEnabled = false + pieChart.setExtraOffsets(5f, 10f, 5f, 5f) + + // Set the pie chart: drag deceleration, hole, hole color + pieChart.setDragDecelerationFrictionCoef(0.95f) + pieChart.isDrawHoleEnabled = true + pieChart.setHoleColor(requireContext().getColor(R.color.primary_bg)) + + // Set the pie chart: transparent circle, hole color, hole radius, draw center text + pieChart.setTransparentCircleColor(requireContext().getColor(R.color.primary_bg)) + pieChart.setTransparentCircleAlpha(110) + pieChart.holeRadius = 42f + pieChart.transparentCircleRadius = 48f + pieChart.setDrawCenterText(true) + + // Set the pie chart: legend and entry label + pieChart.legend.isEnabled = false + pieChart.setEntryLabelColor(Color.BLACK) + pieChart.setEntryLabelTextSize(12f) + + // Set the pie chart: rotation and highlight + pieChart.setRotationAngle(0f) + pieChart.isRotationEnabled = true + pieChart.isHighlightPerTapEnabled = true + } + + private fun setPieData(){ + // Set the pie chart: animation + pieChart.animateY(1400, Easing.EaseInOutQuad) + + // Set the pie chart: data + val entries: ArrayList<PieEntry> = ArrayList() + entries.add(PieEntry(sumIncome, resources.getString(R.string.income))) + entries.add(PieEntry(sumOutcome, resources.getString(R.string.outcome))) + + // Create a pie data set + val dataSet = PieDataSet(entries, resources.getString(R.string.pie_chart_title)) + + // Set the pie data set properties + dataSet.setDrawIcons(false) + dataSet.sliceSpace = 3f + dataSet.iconsOffset = MPPointF(0f, 40f) + dataSet.selectionShift = 5f + + // Set the pie data set: colors + val colors: ArrayList<Int> = ArrayList() + colors.add(requireContext().getColor(R.color.purple_200)) + colors.add(requireContext().getColor(R.color.red)) + dataSet.colors = colors + + // Set the pie data set: text size, text color, text typeface + val data = PieData(dataSet) + data.setValueFormatter(PercentFormatter()) + data.setValueTextSize(15f) + data.setValueTypeface(Typeface.DEFAULT_BOLD) + data.setValueTextColor(Color.BLACK) + pieChart.setData(data) + pieChart.highlightValues(null) + } + + private fun displayPieChart() { + // wait until the income and outcome are calculated + // Observe the LiveData object to update the pie chart when the data is ready + liveDataReady.observe(viewLifecycleOwner) { dataReady -> + if (dataReady) { + // Set the pie data + setPieData() + + // Invalidate the pie chart (refresh) + pieChart.invalidate() + + // Reset the sum of income and outcome + val incomeNumber = binding.totalIncomeNumber + val outcomeNumber = binding.totalOutcomeNumber + + // Update the text views with the formatted values + incomeNumber.text = NumberFormat.getCurrencyInstance(Locale(resources.getString(R.string.language_code), resources.getString(R.string.country_code))).format(sumIncome) + outcomeNumber.text = NumberFormat.getCurrencyInstance(Locale(resources.getString(R.string.language_code), resources.getString(R.string.country_code))).format(sumOutcome) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionAdapter.kt b/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionAdapter.kt index a2a9c178c266735d43c209a65dc6e3c8eaaef612..59629187d9c6e7c7d9348b03ba4d46e0b890c170 100644 --- a/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionAdapter.kt +++ b/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionAdapter.kt @@ -19,6 +19,7 @@ class TransactionAdapter() : RecyclerView.Adapter<TransactionAdapter.Transaction // Create ViewHolder Class private var transactionList : List<Transaction> = emptyList<Transaction>() var onItemClick: ((Transaction) -> Unit)? = null + var onLocationClick: ((String) -> Unit)? = null class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){ val name: TextView = itemView.findViewById(R.id.transactionName) val date: TextView = itemView.findViewById(R.id.transactionDate) @@ -54,6 +55,14 @@ class TransactionAdapter() : RecyclerView.Adapter<TransactionAdapter.Transaction holder.itemView.setOnClickListener{ onItemClick?.invoke(transactionList[position]) } + + holder.location.setOnClickListener { + onLocationClick?.invoke(holder.location.text.toString()) + } + } + + fun getTransactionList() : List<Transaction>{ + return this.transactionList } @SuppressLint("NotifyDataSetChanged") fun setData(transactions: List<Transaction>){ diff --git a/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionFragment.kt b/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionFragment.kt index 14b0558317ff5d3a1d459d7ec05c5061e134296f..238b8660db542b201dd46ca5df9018074760cd43 100644 --- a/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionFragment.kt +++ b/app/src/main/java/com/example/nerbos/fragments/transaction/TransactionFragment.kt @@ -1,14 +1,18 @@ package com.example.nerbos.fragments.transaction -import android.app.AlertDialog +import android.Manifest import android.app.Dialog +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.location.Geocoder +import android.location.Location import android.os.Bundle import android.text.TextUtils -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.Window import android.view.WindowManager import android.widget.Button import android.widget.EditText @@ -17,41 +21,44 @@ import android.widget.RadioButton import android.widget.RadioGroup import android.widget.TextView import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.example.nerbos.MapsActivity import com.example.nerbos.R import com.example.nerbos.model.Transaction import com.example.nerbos.model.TransactionCategory -import com.example.nerbos.viewmodel.TransactionViewModel import com.example.nerbos.service.Authentication import com.example.nerbos.service.Utils +import com.example.nerbos.viewmodel.TransactionViewModel +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import org.apache.poi.hssf.usermodel.HSSFWorkbook +import org.apache.poi.ss.usermodel.Cell +import org.apache.poi.ss.usermodel.Row +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.File +import java.io.FileOutputStream +import java.util.Locale -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [Transaction.newInstance] factory method to - * create an instance of this fragment. - */ class TransactionFragment : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null + private lateinit var transactionViewModel: TransactionViewModel private lateinit var authentication: Authentication + private var currentLocation: Location? = null + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var geocoder: Geocoder + private val permissionCode = 1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + geocoder = Geocoder(requireContext(), Locale.getDefault()) } override fun onCreateView( @@ -67,21 +74,190 @@ class TransactionFragment : Fragment() { recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.adapter = transactionAdapter transactionAdapter.onItemClick = { transaction -> showModifyTransactionDialog(transaction)} + transactionAdapter.onLocationClick = {location -> showLocationOnMap(location)} // Transaction View Model transactionViewModel = ViewModelProvider(this)[TransactionViewModel::class.java] transactionViewModel.setReadDataUserId(authentication.getNim()) - transactionViewModel.readAllData!!.observe(viewLifecycleOwner, Observer { transactions -> - transactionAdapter.setData(transactions) }) + transactionViewModel.readAllData!!.observe(viewLifecycleOwner) { transactions -> + transactionAdapter.setData(transactions) + } + // Add Transaction Button view.findViewById<ImageView>(R.id.addTransactionButton).setOnClickListener { showAddTransactionDialog() } + // Save Transaction + val xlsSave = view.findViewById<TextView>(R.id.xlsSave) + val xlsxSave = view.findViewById<TextView>(R.id.xlsxSave) + + view.findViewById<ImageView>(R.id.saveTransactionsButton).setOnClickListener{ + + if (xlsSave.visibility == View.VISIBLE){ + xlsSave.visibility = View.GONE + xlsxSave.visibility = View.GONE + } else if(xlsSave.visibility == View.GONE){ + xlsSave.visibility = View.VISIBLE + xlsxSave.visibility = View.VISIBLE + } + } + + xlsSave.setOnClickListener { + val workbook: Workbook = createExcelFile(transactionAdapter.getTransactionList(), "xls") + saveWorkBook(workbook, "xls") + xlsSave.visibility = View.GONE + xlsxSave.visibility = View.GONE + Toast.makeText(requireContext(), "Transaction Report.xls is downloaded", Toast.LENGTH_LONG).show() + } + + xlsxSave.setOnClickListener { + val workbook: Workbook = createExcelFile(transactionAdapter.getTransactionList(), "xlsx") + saveWorkBook(workbook, "xlsx") + xlsSave.visibility = View.GONE + xlsxSave.visibility = View.GONE + Toast.makeText(requireContext(), "Transaction Report.xlsx is downloaded", Toast.LENGTH_LONG).show() + } + + // Send Transaction + val xlsSend = view.findViewById<TextView>(R.id.xlsSend) + val xlsxSend = view.findViewById<TextView>(R.id.xlsxSend) + + view.findViewById<ImageView>(R.id.sendTransactionsButton).setOnClickListener{ + if (xlsSend.visibility == View.VISIBLE){ + xlsSend.visibility = View.GONE + xlsxSend.visibility = View.GONE + } else if(xlsSend.visibility == View.GONE){ + xlsSend.visibility = View.VISIBLE + xlsxSend.visibility = View.VISIBLE + } + } + + xlsSend.setOnClickListener { + val subject = getString(R.string.email_subject) + val message = getString(R.string.email_message) + val workbook: Workbook = createExcelFile(transactionAdapter.getTransactionList(), "xls") + sendWorkbook(authentication.getEmail(), subject, message, workbook, "xls") + xlsSend.visibility = View.GONE + xlsxSend.visibility = View.GONE + } + + xlsxSend.setOnClickListener { + val subject = getString(R.string.email_subject) + val message = getString(R.string.email_message) + val workbook: Workbook = createExcelFile(transactionAdapter.getTransactionList(), "xlsx") + sendWorkbook(authentication.getEmail(), subject, message, workbook, "xlsx") + xlsSend.visibility = View.GONE + xlsxSend.visibility = View.GONE + } + return view } + private fun createExcelFile(transactions : List<Transaction>, extension: String) : Workbook{ + val workbook : Workbook + val sheet : Sheet + if (extension == "xls"){ + workbook = HSSFWorkbook() + sheet = workbook.createSheet("My Transactions") + } else { // "xlsx" + workbook = XSSFWorkbook() + sheet = workbook.createSheet("My Transactions") + } + + // Setup Header + val header : Row = sheet.createRow(0) + var cell : Cell = header.createCell(0) + cell.setCellValue("Date") + cell = header.createCell(1) + cell.setCellValue("Category") + cell = header.createCell(2) + cell.setCellValue("Nominal") + cell = header.createCell(3) + cell.setCellValue("Name") + cell = header.createCell(4) + cell.setCellValue("Location") + + // Fill the cell for transaction data + for (i in transactions.indices){ + val row : Row= sheet.createRow(i+1) + cell = row.createCell(0) + cell.setCellValue(transactions[i].date.toString()) + cell = row.createCell(1) + cell.setCellValue(transactions[i].category.toString()) + cell = row.createCell(2) + cell.setCellValue(transactions[i].nominal.toDouble()) + cell = row.createCell(3) + cell.setCellValue(transactions[i].name) + cell = row.createCell(4) + cell.setCellValue(transactions[i].location) + } + + return workbook + } + private fun saveWorkBook(workbook: Workbook, extension: String) { + val appDirectory = requireContext().filesDir + + val filepath = File(appDirectory , "Transaction Report.$extension") + + try{ + if(!filepath.exists()){ + filepath.createNewFile() + } + val fileOutputStream = FileOutputStream(filepath) + workbook.write(fileOutputStream) + fileOutputStream.flush() + fileOutputStream.close() + + }catch (e:Exception){ + e.printStackTrace() + } + } + + private fun sendWorkbook(email: String, subject: String, message: String, workbook: Workbook, extension: String) { + val file = File(requireContext().cacheDir, "Transaction Report.$extension") + + try { + val fileOutputStream = FileOutputStream(file) + workbook.write(fileOutputStream) + fileOutputStream.flush() + fileOutputStream.close() + + val intent = Intent(Intent.ACTION_SEND) + val uri = FileProvider.getUriForFile(requireContext(), requireContext().packageName + ".provider", file) + intent.type = "message/rfc822" + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, message) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + // Grant URI permission to the chosen email application + val chooserIntent = Intent.createChooser(intent, "Send Email") + val resInfoList: List<ResolveInfo> = requireContext().packageManager.queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY) + for (resolveInfo in resInfoList) { + val packageName = resolveInfo.activityInfo.packageName + requireContext().grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + try { + startActivity(chooserIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(requireContext(), "No email client installed", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(requireContext(), "Failed to send file", Toast.LENGTH_SHORT).show() + } + } + + private fun showLocationOnMap(location: String) { + val mapIntent = Intent(requireActivity(), MapsActivity::class.java) + mapIntent.putExtra("locationName", location) + startActivity(mapIntent) + } + private fun showAddTransactionDialog() { val dialog = Dialog(requireContext()) @@ -102,6 +278,12 @@ class TransactionFragment : Fragment() { dialog.dismiss() } + dialog.findViewById<EditText>(R.id.locationInput).setOnClickListener { + // TODO: Buat percabangan kalau ada internet + val etLocation= dialog.findViewById<EditText>(R.id.locationInput) + etLocation.setText(getAddressName()) + } + dialog.show() dialog.window!!.setBackgroundDrawableResource(R.drawable.round_corner_transparent) dialog.window!!.attributes = layoutParams @@ -190,7 +372,7 @@ class TransactionFragment : Fragment() { val categoryString : String = radioButton.text.toString() - val userID: Int = authentication.getNim(); + val userID: Int = authentication.getNim() val name : String = view.findViewById<EditText>(R.id.nameInput).text.toString() var category : TransactionCategory = TransactionCategory.INCOME if (categoryString === this.getString(R.string.income)){ @@ -214,28 +396,38 @@ class TransactionFragment : Fragment() { } } - private fun inputCheck(name: String, nominal:Float, location:String): Boolean { - return !(TextUtils.isEmpty(name) || TextUtils.isEmpty(location) || (nominal<0) ) + private fun setLocation(){ + 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), permissionCode) + return + } + fusedLocationProviderClient.lastLocation.addOnSuccessListener (requireActivity()){ + location -> + currentLocation = location + } } - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment Transaction. - */ - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - TransactionFragment().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } - } + @Suppress("DEPRECATION") + private fun getAddressName() : String{ + // Hanya set otomatis jika mendapatkan permission, jika tidak input lokasi string + setLocation() + return if (currentLocation!=null){ + // Geocode to get the address string + val address = geocoder.getFromLocation(currentLocation!!.latitude, currentLocation!!.longitude, 1) + address!![0].getAddressLine(0) + } else { + "" + } } + private fun inputCheck(name: String, nominal:Float, location:String): Boolean { + return !(TextUtils.isEmpty(name) || TextUtils.isEmpty(location) || (nominal<0) ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt b/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt index 167b34ac67800ba787e7192bab4421ad3152a1f9..378a6f2e1af9fda80cf7a9e5d2f51c5367ce8c23 100644 --- a/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt +++ b/app/src/main/java/com/example/nerbos/repository/TransactionRepository.kt @@ -3,6 +3,7 @@ package com.example.nerbos.repository import androidx.lifecycle.LiveData import com.example.nerbos.model.Transaction import com.example.nerbos.data.TransactionDao +import com.example.nerbos.model.TransactionCategory import com.example.nerbos.service.Authentication class TransactionRepository(private val transactionDao: TransactionDao) { @@ -22,4 +23,8 @@ class TransactionRepository(private val transactionDao: TransactionDao) { fun setReadAllDataUserId(userID: Int){ readAllData = transactionDao.readAllData(userID) } + + fun getSum(userID: Int, category: TransactionCategory): LiveData<Float>{ + return transactionDao.getSum(userID, category) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/nerbos/service/Authentication.kt b/app/src/main/java/com/example/nerbos/service/Authentication.kt index 62cde4837bfdccf20a592ff3b52445bf623e1450..ca362b71f0e9ecefbb3d227e2b12bc23aaee4016 100644 --- a/app/src/main/java/com/example/nerbos/service/Authentication.kt +++ b/app/src/main/java/com/example/nerbos/service/Authentication.kt @@ -84,7 +84,7 @@ class Authentication(private val context: Context) { return email.toInt() } - private fun getToken(): String { + internal fun getToken(): String { val sharedPreferences : SharedPreferences = context.getSharedPreferences(context.getString(R.string.preferences), Context.MODE_PRIVATE) return sharedPreferences.getString(context.getString(R.string.token), "") ?: "" diff --git a/app/src/main/java/com/example/nerbos/viewmodel/TransactionViewModel.kt b/app/src/main/java/com/example/nerbos/viewmodel/TransactionViewModel.kt index 50081922b59de5349c14327404e84efdf8e8cca5..ed27c0a71f60129bf3fdd5443f84a60a97dac15a 100644 --- a/app/src/main/java/com/example/nerbos/viewmodel/TransactionViewModel.kt +++ b/app/src/main/java/com/example/nerbos/viewmodel/TransactionViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import com.example.nerbos.model.Transaction import com.example.nerbos.data.TransactionDatabase +import com.example.nerbos.model.TransactionCategory import com.example.nerbos.repository.TransactionRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -41,4 +42,8 @@ class TransactionViewModel(application: Application): AndroidViewModel(applicati repository.setReadAllDataUserId(userId) readAllData = repository.readAllData } + + fun getSums(userId: Int, category: TransactionCategory): LiveData<Float>{ + return repository.getSum(userId, category) + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/border_save_selection.xml b/app/src/main/res/drawable/border_save_selection.xml new file mode 100644 index 0000000000000000000000000000000000000000..8983d53e9a6d52fbc06654159d8ba3fb615ab910 --- /dev/null +++ b/app/src/main/res/drawable/border_save_selection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <shape android:shape="rectangle"> + <solid android:color="@color/base_bg" /> + </shape> + </item> + <item android:bottom="2dp"> + <shape android:shape="rectangle"> + <solid android:color="@color/white"/> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/button_save.png b/app/src/main/res/drawable/button_save.png new file mode 100644 index 0000000000000000000000000000000000000000..14956f537e47b091899be3f8065e3bd6994f2570 Binary files /dev/null and b/app/src/main/res/drawable/button_save.png differ diff --git a/app/src/main/res/drawable/button_send.png b/app/src/main/res/drawable/button_send.png new file mode 100644 index 0000000000000000000000000000000000000000..2a201a3dbd4d4953bb3cce8089a952608fa62835 Binary files /dev/null and b/app/src/main/res/drawable/button_send.png differ diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000000000000000000000000000000000000..15e0dcca3b0376164ca26f1b1648fe223aa976f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M11.67,3.87L9.9,2.1 0,12l9.9,9.9 1.77,-1.77L3.54,12z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 0000000000000000000000000000000000000000..f974c6002e895d87f9ea13cdc3e4d052be61018c --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="15dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="15dp"> + + <path android:fillColor="@android:color/white" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2z"/> + +</vector> diff --git a/app/src/main/res/drawable/round_corner_save_selection.xml b/app/src/main/res/drawable/round_corner_save_selection.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fc1858f38806ae67f85fb888c452f48786efc33 --- /dev/null +++ b/app/src/main/res/drawable/round_corner_save_selection.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/white" /> + <corners android:bottomLeftRadius="10dp" android:bottomRightRadius="10dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 8a9966c9694727aa195f1036c7838296f313ff57..f2c30fb6f6ed0c1150c1c9546eae3ec31f650df6 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -11,7 +11,6 @@ android:id="@+id/appBarLayout" android:layout_width="0dp" android:layout_height="wrap_content" - android:background="@color/primary_bg" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -45,16 +44,13 @@ android:layout_width="78dp" android:layout_height="0dp" - android:background="@color/primary_bg" + android:background="@color/navbar_bg" app:elevation="1dp" app:itemBackground="@color/navbar_bg" app:itemIconTint="@color/navbar_icon" - app:itemTextColor="@color/navbar_icon" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" app:layout_constraintTop_toBottomOf="@+id/appBarLayout" - app:layout_constraintVertical_bias="1.0" app:menu="@menu/bottom_navbar"> </com.google.android.material.navigation.NavigationView> diff --git a/app/src/main/res/layout-land/fragment_statistic.xml b/app/src/main/res/layout-land/fragment_statistic.xml new file mode 100644 index 0000000000000000000000000000000000000000..b810818485e21b56567ea789c689b1d4130f0717 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_statistic.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout + 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="match_parent" + android:fillViewport="true" + android:background="@color/primary_bg" + tools:context=".fragments.statistic.StatisticFragment"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/titleChart" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="44dp" + android:gravity="center" + android:padding="10dp" + android:text="@string/pie_chart_title" + android:textAlignment="center" + android:textColor="@color/chart_title" + android:textSize="24sp" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/pieChart" + app:layout_constraintTop_toTopOf="parent" /> + + <com.github.mikephil.charting.charts.PieChart + android:id="@+id/pieChart" + android:layout_width="300dp" + android:layout_height="300dp" + android:layout_marginStart="52dp" + android:layout_weight="1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" /> + + <TextView + android:id="@+id/totalIncome" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_marginStart="8dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/income" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:drawableLeftCompat="@drawable/ic_circle" + app:drawableTint="@color/purple_200" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/totalOutcome" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/pieChart" + app:layout_constraintTop_toBottomOf="@+id/titleChart" + app:layout_constraintVertical_bias="0.2" /> + + <TextView + android:id="@+id/totalOutcome" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/outcome" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:drawableLeftCompat="@drawable/ic_circle" + app:drawableTint="@color/red" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/totalIncome" + app:layout_constraintTop_toBottomOf="@+id/titleChart" + app:layout_constraintVertical_bias="0.2" /> + + <TextView + android:id="@+id/totalIncomeNumber" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_marginStart="20dp" + android:layout_marginTop="16dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/income" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/totalOutcomeNumber" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/pieChart" + app:layout_constraintTop_toBottomOf="@+id/totalIncome" + app:layout_constraintVertical_bias="0.2" /> + + <TextView + android:id="@+id/totalOutcomeNumber" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_marginTop="24dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/outcome" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/totalIncomeNumber" + app:layout_constraintTop_toBottomOf="@+id/totalOutcome" + app:layout_constraintVertical_bias="0.200" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_user.xml b/app/src/main/res/layout-land/fragment_user.xml new file mode 100644 index 0000000000000000000000000000000000000000..05f64b5008a810ea923ad11127399d146e39a86d --- /dev/null +++ b/app/src/main/res/layout-land/fragment_user.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout 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="match_parent" + tools:context=".fragments.user.UserFragment" + android:background="@color/primary_bg"> + + <!-- TODO: Update blank fragment layout --> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/primary_bg"> + + <androidx.cardview.widget.CardView + android:id="@+id/logoutCard" + android:layout_width="400dp" + android:layout_height="310dp" + app:cardBackgroundColor="@color/login_card_bg" + app:cardCornerRadius="20dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5"> + + <TextView + android:id="@+id/logoutTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="15dp" + android:gravity="center_horizontal" + android:text="@string/welcome" + android:textColor="@color/white" + android:textSize="40sp" + android:visibility="visible" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/logoutDescription" + android:layout_width="341dp" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:gravity="center_horizontal" + android:text="@string/app_name_full" + android:textColor="@color/text_secondary" + android:textSize="20sp" + android:visibility="visible" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/logoutTitle" /> + + <androidx.cardview.widget.CardView + android:id="@+id/cardView4" + android:layout_width="330dp" + android:layout_height="50dp" + android:layout_gravity="center_horizontal" + app:cardBackgroundColor="@color/login_input_field" + app:cardCornerRadius="20dp" + app:layout_constraintBottom_toTopOf="@+id/cardView5" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.548" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/logoutDescription" + app:layout_constraintVertical_bias="0.361"> + + <TextView + android:id="@+id/textViewUser" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="" + android:textColor="@color/white" + android:textSize="20sp" /> + + </androidx.cardview.widget.CardView> + + <androidx.cardview.widget.CardView + android:id="@+id/cardView5" + android:layout_width="330dp" + android:layout_height="50dp" + android:layout_gravity="center_horizontal" + android:layout_marginTop="75dp" + app:cardBackgroundColor="@color/login_input_field" + app:cardCornerRadius="20dp" + app:layout_constraintBottom_toTopOf="@+id/button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.548" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/logoutDescription" + app:layout_constraintVertical_bias="0.0"> + + <TextView + android:id="@+id/textViewEmail" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="" + android:textColor="@color/white" + android:textSize="20sp" /> + + </androidx.cardview.widget.CardView> + + <Button + android:id="@+id/button" + android:layout_width="130dp" + android:layout_height="55dp" + android:layout_gravity="center_horizontal" + android:layout_marginTop="135dp" + android:labelFor="@id/button" + android:text="@string/logout_title" + android:textColor="@color/white" + android:textSize="20sp" + app:backgroundTint="@color/dark_red" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.497" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/logoutDescription" + app:layout_constraintVertical_bias="0.1" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.cardview.widget.CardView> + + </androidx.constraintlayout.widget.ConstraintLayout> +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_maps.xml b/app/src/main/res/layout/activity_maps.xml new file mode 100644 index 0000000000000000000000000000000000000000..827f664a41139e37d9f993860a71e7afeac921b6 --- /dev/null +++ b/app/src/main/res/layout/activity_maps.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".MapsActivity" + > + + <fragment + xmlns:map="http://schemas.android.com/apk/res-auto" + android:id="@+id/map" + android:name="com.google.android.gms.maps.SupportMapFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + map:layout_constraintTop_toTopOf="parent" + /> + <TextView + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="24sp" + android:textColor="@color/white" + android:background="@color/base_bg" + android:text="@string/maps" + android:paddingTop = "5dp" + android:paddingStart="40dp" + android:paddingEnd="10dp" + android:paddingBottom="10dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/backButton" + /> + + <ImageView + android:id="@+id/backButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:paddingBottom="5dp" + android:src="@drawable/ic_back" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + /> +</androidx.constraintlayout.widget.ConstraintLayout> 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 diff --git a/app/src/main/res/layout/fragment_scan.xml b/app/src/main/res/layout/fragment_scan.xml index 193a7384f6056c7253520fb441d76721006f1626..3d5bb3139356fbef43e87b7008307190a98e0070 100644 --- a/app/src/main/res/layout/fragment_scan.xml +++ b/app/src/main/res/layout/fragment_scan.xml @@ -3,14 +3,66 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/primary_bg" tools:context=".fragments.scan.ScanFragment"> - <!-- TODO: Update blank fragment layout --> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Scan Fragment" - android:textSize="26dp" - android:layout_gravity="center" /> + <LinearLayout + android:id="@+id/scanVerticalLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:ignore="UselessParent"> + + <Space + android:layout_width="match_parent" + android:layout_height="50dp" /> + + <androidx.camera.view.PreviewView + android:id="@+id/previewView" + android:layout_width="300dp" + android:layout_height="400dp" + android:layout_gravity="center" /> + + <LinearLayout + android:id="@+id/scanHorizontalLayout" + android:layout_width="300dp" + android:layout_height="160dp" + android:layout_gravity="center" + android:orientation="horizontal"> + + <ImageButton + android:id="@+id/captureButton" + android:layout_width="75dp" + android:layout_height="75dp" + android:layout_gravity="center" + android:background="@drawable/round_corner_button_weak" + android:contentDescription="@string/capture_button" + android:scaleType="fitCenter" + android:src="@android:drawable/ic_menu_camera" + tools:ignore="RedundantDescriptionCheck" /> + + <Space + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" /> + + <ImageButton + android:id="@+id/galleryButton" + android:layout_width="75dp" + android:layout_height="75dp" + android:layout_gravity="center" + android:background="@drawable/round_corner_button_weak" + android:contentDescription="@string/gallery_button" + android:scaleType="fitCenter" + android:src="@android:drawable/ic_menu_gallery" + tools:ignore="RedundantDescriptionCheck" /> + + </LinearLayout> + + <Space + android:layout_width="match_parent" + android:layout_height="125dp" /> + + </LinearLayout> </FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_statistic.xml b/app/src/main/res/layout/fragment_statistic.xml index c90a5c1c7ad6b85e52a6d0f3902bd80803bab3f1..ba705c87f7af0ed44241908f4128ca57beb58cb8 100644 --- a/app/src/main/res/layout/fragment_statistic.xml +++ b/app/src/main/res/layout/fragment_statistic.xml @@ -1,16 +1,120 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout + 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="match_parent" + android:fillViewport="true" + android:background="@color/primary_bg" tools:context=".fragments.statistic.StatisticFragment"> - <!-- TODO: Update blank fragment layout --> - <TextView - android:layout_width="wrap_content" + <RelativeLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Statistic Fragment" - android:textSize="26dp" - android:layout_gravity="center" /> + android:layout_marginTop="0dp" + android:layout_gravity="center" + tools:ignore="UselessParent"> + + <TextView + android:id="@+id/titleChart" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:padding="10dp" + android:text="@string/pie_chart_title" + android:textAlignment="center" + android:textColor="@color/chart_title" + android:textSize="25sp" + android:textStyle="bold" /> + + + <!--Ui component for our pie chart--> + <com.github.mikephil.charting.charts.PieChart + android:id="@+id/pieChart" + android:layout_width="300dp" + android:layout_height="300dp" + android:layout_below="@+id/titleChart" + android:layout_alignParentEnd="false" + android:layout_centerHorizontal="true" + android:layout_marginTop="20dp" + android:layout_marginBottom="5dp" /> + + <LinearLayout + android:id="@+id/totalIncomeOutcome" + android:layout_width="300dp" + android:layout_height="wrap_content" + android:layout_below="@+id/pieChart" + android:layout_marginTop="25dp" + android:orientation="horizontal" + android:layout_centerHorizontal="true" + android:weightSum="2"> + + <TextView + android:id="@+id/totalIncome" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/income" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:drawableTint="@color/purple_200" + app:drawableLeftCompat="@drawable/ic_circle" /> + + <TextView + android:id="@+id/totalOutcome" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/outcome" + android:textAlignment="center" + android:textColor="@color/chart_title" + app:drawableLeftCompat="@drawable/ic_circle" + app:drawableTint="@color/red" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/totalIncomeOutcomeNumber" + android:layout_width="300dp" + android:layout_height="wrap_content" + android:layout_below="@+id/totalIncomeOutcome" + android:layout_marginTop="15dp" + android:orientation="horizontal" + android:layout_centerHorizontal="true" + android:weightSum="2"> + + <TextView + android:id="@+id/totalIncomeNumber" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/income" + android:textAlignment="center" + android:textColor="@color/chart_title" /> + + <TextView + android:id="@+id/totalOutcomeNumber" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:layout_weight="1" + android:gravity="center" + android:padding="4dp" + android:text="@string/outcome" + android:textAlignment="center" + android:textColor="@color/chart_title" /> + + </LinearLayout> + + </RelativeLayout> </FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_transaction.xml b/app/src/main/res/layout/fragment_transaction.xml index 690063bd5c7b9d44d6d2de81aa99a259b4343789..10f6f89ddcf185f2a21e3e14a69b5a0a5a7aa234 100644 --- a/app/src/main/res/layout/fragment_transaction.xml +++ b/app/src/main/res/layout/fragment_transaction.xml @@ -13,20 +13,40 @@ android:background="@color/base_bg" android:padding="20dp" > + <ImageView + android:id="@+id/saveTransactionsButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/button_save" + android:layout_marginStart="10dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="@string/save_transactions_button" /> + + <ImageView + android:id="@+id/sendTransactionsButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/button_send" + android:layout_marginEnd="10dp" + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:contentDescription="@string/send_transactions_button" /> <TextView android:id="@+id/myTransaction" android:layout_width="0sp" android:layout_height="wrap_content" - android:text="My Transaction" + android:text="@string/my_transaction" android:textColor="@color/white" android:textSize="20sp" android:textStyle="bold" android:layout_marginStart="10dp" - android:layout_marginTop="8dp" + android:layout_marginTop="40dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toBottomOf="@+id/saveTransactionsButton" /> <ImageView @@ -34,12 +54,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/plus" - app:layout_constraintTop_toTopOf="parent" + android:layout_marginTop="30dp" + app:layout_constraintTop_toBottomOf="@id/sendTransactionsButton" app:layout_constraintEnd_toEndOf="parent" - > </ImageView> - - - + android:contentDescription="@string/add_transaction_button"/> <androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" @@ -49,5 +67,81 @@ app:layout_constraintTop_toBottomOf="@+id/myTransaction" /> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginStart="28dp" + android:background="@drawable/round_corner_save_selection" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/saveTransactionsButton" > + + <TextView + android:id="@+id/xlsSave" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/xls" + android:background="@drawable/border_save_selection" + android:textSize="18sp" + android:textColor="@color/base_bg" + android:paddingTop="2dp" + android:paddingStart="30dp" + android:paddingBottom="4dp" + android:paddingEnd="30dp" + android:visibility="gone" + /> + + <TextView + android:id="@+id/xlsxSave" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="18sp" + android:textColor="@color/base_bg" + android:text="@string/xlsx" + android:paddingBottom="3dp" + android:paddingStart="26dp" + android:paddingEnd="30dp" + android:visibility="gone" + /> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginStart="290dp" + android:background="@drawable/round_corner_save_selection" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/sendTransactionsButton" > + + <TextView + android:id="@+id/xlsSend" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/xls" + android:background="@drawable/border_save_selection" + android:textSize="18sp" + android:textColor="@color/base_bg" + android:paddingTop="2dp" + android:paddingStart="30dp" + android:paddingBottom="4dp" + android:paddingEnd="30dp" + android:visibility="gone" + /> + + <TextView + android:id="@+id/xlsxSend" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="18sp" + android:textColor="@color/base_bg" + android:text="@string/xlsx" + android:paddingBottom="3dp" + android:paddingStart="26dp" + android:paddingEnd="30dp" + android:visibility="gone" + /> + </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> </FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 20f56ac656d92d996a3a20df82ea73e84d0f8d30..e702056a76d75222618a58d672ef2d14ae8feead 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -20,4 +20,15 @@ <color name="strong_blue_button">#0DA6C2</color> <color name="weak_blue_button">#800DA6C2</color> <color name="transparent">#00FFFFFF</color> + + <color name="purple_200">#0F9D58</color> + <color name="purple_500">#0F9D58</color> + <color name="purple_700">#0F9D58</color> + <color name="teal_200">#FF03DAC5</color> + <color name="teal_700">#FF018786</color> + <color name="grey">#aaa</color> + <color name="button_color">#1E573B</color> + <color name="yellow">#FFEB3B</color> + <color name="chart_title">#EAE9F6</color> + </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93156b4b00eeac9c58eece350969a1b530d0fa4a..4850983e3b3a6d9ef0f9a0685b30a9efdcb5edb7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ <string name="backend_api_login">https://pbd-backend-2024.vercel.app/api/auth/login</string> <string name="backend_api_token">https://pbd-backend-2024.vercel.app/api/auth/token</string> + <string name="backend_api_scan">https://pbd-backend-2024.vercel.app/api/bill/upload</string> <string name="ok">OK</string> <string name="welcome">Welcome!</string> @@ -46,7 +47,25 @@ <string name="delete_success">Transaction successfully deleted</string> <string name="income">Income</string> <string name="outcome">Outcome</string> + <string name="title_activity_maps">MapsActivity</string> + <string name="maps">Maps</string> <string name="network_sensing_string">No Internet Connection Please check your connection and try again.</string> <string name="close_button">Close Button</string> + <string name="pie_chart">Pie Chart</string> + <string name="my_transaction">My Transaction</string> + <string name="add_transaction_button">Add Transaction Button</string> + <string name="send">Send</string> + <string name="save_transactions_button">Save Transactions Button</string> + <string name="send_transactions_button">Send Transactions Button</string> + <string name="xls">.xls</string> + <string name="xlsx">.xlsx</string> + <string name="pie_chart_title">Income vs Outcome Chart</string> + <string name="language_code">id</string> + <string name="country_code">ID</string> + <string name="capture_button">Capture Button</string> + <string name="gallery_button">Gallery Button</string> + + <string name="email_subject">NerbOS - Transaction Report</string> + <string name="email_message">Here is your transaction report</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..660be4143fc96a9d2411975cb9188a85a6b79a43 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths> + <cache-path + name="cache" + path="/" /> +</paths> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 98de0e48fee24c2754decc4211bfc712e0299e5f..1764a12d184f94f866991078a28422563e8d4935 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "8.3.1" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031eb7d63f785752b1914cc8692a453d1cc63..2e7e7ed807f16f684be27f3b0959e2342e346381 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f4f7ca044d42341d3ea2d11ed06fa2983bcacc3f..f677740f90476b45d0068ed3e06d785a3e0b6271 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() + maven { url = uri("https://jitpack.io") } mavenCentral() } }