diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58d9bf3daeef791ce0866be18d545d715a31703c..9beff3ec9f2a87177ae5d8fe7e4221c43fd798af 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") } android { @@ -42,6 +43,8 @@ android { dependencies { + implementation("com.google.android.gms:play-services-location:21.2.0") + val roomVersion = "2.6.1" implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.appcompat:appcompat:1.5.1") @@ -72,6 +75,9 @@ dependencies { implementation("io.github.evanrupert:excelkt:1.0.2") + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 524104533e9a21551af37a80fdf31a32e97deb80..7847d48ad6e1766381886bab7755aa7d1395799d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,10 +2,13 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application + android:name=".ui.transactions.TransactionsApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" @@ -23,6 +26,15 @@ android:permission="android.permission.INTERNET"> </service> + <receiver + android:name=".ui.transactions.TransactionsBroadcastReceiver" + android:enabled="true" + android:exported="false"> + <intent-filter> + <action android:name="com.BondoYap.transactions.randomize" /> + </intent-filter> + </receiver> + <activity android:name=".ui.login.LoginActivity" android:screenOrientation="portrait" @@ -39,6 +51,8 @@ android:exported="true"> </activity> + + </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/MainActivity.kt b/app/src/main/java/com/example/bondoyap/MainActivity.kt index bb0a8bfb7024ffb846255ee25e928a26a381d598..f99a955ca06223a0f5dc78aba72f6cc75a909984 100644 --- a/app/src/main/java/com/example/bondoyap/MainActivity.kt +++ b/app/src/main/java/com/example/bondoyap/MainActivity.kt @@ -2,23 +2,38 @@ package com.example.bondoyap import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.SharedPreferences import android.os.Bundle +import androidx.activity.viewModels import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContentProviderCompat.requireContext +import androidx.fragment.app.viewModels +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME import com.example.bondoyap.databinding.ActivityMainBinding +import com.example.bondoyap.service.api.Constants import com.example.bondoyap.service.jwt.JwtService import com.example.bondoyap.ui.login.LoginActivity +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsBroadcastReceiver +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var sharedPreferences: SharedPreferences + private lateinit var transactionsBroadcastReceiver: TransactionsBroadcastReceiver + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((applicationContext as TransactionsApplication).repository) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,8 +63,22 @@ class MainActivity : AppCompatActivity() { setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) + + transactionsBroadcastReceiver = TransactionsBroadcastReceiver(transactionsViewModel) + val filter = IntentFilter(Constants.ACTION_RANDOMIZE_TRANSACTIONS) + LocalBroadcastManager.getInstance(this).registerReceiver(transactionsBroadcastReceiver, filter) + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_activity_main) + return navController.navigateUp() || super.onSupportNavigateUp() } private fun isLoggedIn(): Boolean { return sharedPreferences.getBoolean("isLoggedIn", false) } + + override fun onDestroy() { + super.onDestroy() + LocalBroadcastManager.getInstance(this).unregisterReceiver(transactionsBroadcastReceiver) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/Constants.kt b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt index 76fb345217e505ad47427a35808c9e2a256b86ef..897daedea631dae9a33ff6c0eca3671394eb39df 100644 --- a/app/src/main/java/com/example/bondoyap/service/api/Constants.kt +++ b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt @@ -3,4 +3,5 @@ package com.example.bondoyap.service.api object Constants { const val BASE_URL: String = "https://pbd-backend-2024.vercel.app/api/" const val SHARED_PREFS_NAME = "BondoYap" + const val ACTION_RANDOMIZE_TRANSACTIONS = "com.BondoYap.transactions.randomize" } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt index e64b43e08ad5bb67e76fc8892e9ef61387d0e9ee..e21b23e4fa0c8c171cb0f1e56f67f635e6dae6b2 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt @@ -49,6 +49,14 @@ class LoginActivity : AppCompatActivity() { loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java] // skip login + loginViewModel.login( + "13521xxx@std.stei.itb.ac.id", + "password_13521xxx" + ) + if (isLoggedIn()) { + navigateToMainActivity() + finish() + } loginViewModel.loginFormState.observe(this@LoginActivity, Observer { val loginState = it ?: return@Observer diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt index 4e59685263b89adacc8550050b17a4e34b81a240..9ad022210b1304cd3da12e25f6aee6c81062a29e 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt @@ -2,13 +2,16 @@ package com.example.bondoyap.ui.settings import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.example.bondoyap.databinding.FragmentSettingsBinding +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS import com.example.bondoyap.ui.login.LoginActivity class SettingsFragment : Fragment() { @@ -25,6 +28,7 @@ class SettingsFragment : Fragment() { _binding = FragmentSettingsBinding.inflate(inflater, container, false) return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -60,13 +64,15 @@ class SettingsFragment : Fragment() { randomButton.setOnClickListener { Toast.makeText(appContext, "Membuat transaksi random ...", Toast.LENGTH_SHORT).show() - //todo - } - val database = MockDatabase() - val exporter = TransactionsExporter(database) + val intent = Intent(ACTION_RANDOMIZE_TRANSACTIONS) + intent.putExtra("message", "Randomize from setting!") + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + + Log.d("BroadcastDebug", "Sending broadcast from SettingsFragment") + } - saveButton.setOnClickListener{ + saveButton.setOnClickListener { Toast.makeText(appContext, "Menyimpan transaksi...", Toast.LENGTH_SHORT).show() //todo choose one exporter.exportToXLS() @@ -75,7 +81,7 @@ class SettingsFragment : Fragment() { Toast.makeText(appContext, "Penyimpanan pada folder Documents berhasil...", Toast.LENGTH_SHORT).show() } - sendButton.setOnClickListener{ + sendButton.setOnClickListener { Toast.makeText(appContext, "Mengirimkan transaksi ...", Toast.LENGTH_SHORT).show() //todo } diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..6463fa0928a6487d79dfda7d59da716d1f735aed --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt @@ -0,0 +1,143 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.content.Context +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.example.bondoyap.databinding.FragmentAddTransactionsBinding +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class AddTransactionsFragment : Fragment() { + + private var _binding: FragmentAddTransactionsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var latitude: String + private lateinit var longitude: String + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddTransactionsBinding.inflate(inflater, container, false) + + val pemasukan = "Pemasukan" + val pengeluaran = "Pengeluaran" + + val categories = arrayOf(pemasukan, pengeluaran) + val adapter = ArrayAdapter(requireContext(), R.layout.simple_spinner_item, categories) + + adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + if ( + ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(requireActivity(), + arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + } + + binding.buttonSimpan.setOnClickListener { + val isPemasukan: Boolean = when (binding.spinnerKategori.selectedItem.toString()) { + pemasukan -> true + else -> false + } + + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val currentDate = dateFormat.format(Date()) + + val judul = if (binding.editTextJudul.text.toString().trim().isNotEmpty()) { + binding.editTextJudul.text.toString() + } else { + "Untitled" + } + + val nominal = if (binding.editTextNominal.text.toString().trim().isNotEmpty()) { + binding.editTextNominal.text.toString().toDouble() + } else { + 0.0 + } + + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { + if(it != null) { + latitude = it.latitude.toString() + longitude = it.longitude.toString() + + val transaction: Transactions = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = longitude, + latitude = latitude + ) + transactionsViewModel.upsert(transaction) + } + } + + if( + ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + val transaction: Transactions = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + + Toast.makeText(requireContext(), "Transaksi berhasil disimpan", Toast.LENGTH_SHORT).show() + + binding.editTextJudul.text.clear() + binding.editTextNominal.text.clear() + binding.spinnerKategori.setSelection(0) + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..f72aaec034e554588d337ae3b8a2d80d30291e71 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt @@ -0,0 +1,246 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.app.AlertDialog +import android.content.pm.PackageManager +import android.location.Address +import android.location.Geocoder +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.SpannableStringBuilder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.databinding.FragmentEditTransactionsBinding +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import java.util.Locale + +class EditTransactionsFragment : Fragment() { + + private var _binding: FragmentEditTransactionsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEditTransactionsBinding.inflate(inflater, container, false) + + var tanggal: String = "" + var latitude = "" + var longitude = "" + + val pemasukan = "Pemasukan" + val pengeluaran = "Pengeluaran" + + val categories = arrayOf(pemasukan, pengeluaran) + val adapter = ArrayAdapter(requireContext(), R.layout.simple_spinner_item, categories) + + adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + + binding.spinnerKategori.isEnabled = false + binding.editTextLokasi.isEnabled = false + + val transactionId: Int = arguments?.getInt("transaction_id") ?: -1 + + CoroutineScope(Main).launch { + + val originalTransaction: Transactions = transactionsViewModel.get(transactionId) + + binding.editTextJudul.text = SpannableStringBuilder(originalTransaction.judul) + binding.editTextNominal.text = SpannableStringBuilder(originalTransaction.nominal.toBigDecimal().toString()) + + if (originalTransaction.isPemasukan) { + binding.spinnerKategori.setSelection(categories.indexOf(pemasukan)) + } else { + binding.spinnerKategori.setSelection(categories.indexOf(pengeluaran)) + } + + tanggal = originalTransaction.tanggal + longitude = originalTransaction.longitude + latitude = originalTransaction.latitude + + if (originalTransaction.longitude.isEmpty() || originalTransaction.latitude.isEmpty()) { + binding.editTextLokasi.text = SpannableStringBuilder("Unavailable") + } else { + val addresses: List<Address> = + Geocoder(requireContext(), Locale.getDefault()).getFromLocation( + originalTransaction.latitude.toDouble(), + originalTransaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + binding.editTextLokasi.text = SpannableStringBuilder(locationName) + } + } + } + } + + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + if ( + ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(requireActivity(), + arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + } + + binding.checkboxUpdateLokasi.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if ( + ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions(requireActivity(), + arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + binding.checkboxUpdateLokasi.isChecked = false + } + } + } + + binding.buttonUpdate.setOnClickListener { + val isPemasukan: Boolean = when (binding.spinnerKategori.selectedItem.toString()) { + pemasukan -> true + else -> false + } + + val judul = if (binding.editTextJudul.text.toString().trim().isNotEmpty()) { + binding.editTextJudul.text.toString() + } else { + "Untitled" + } + + val nominal = if (binding.editTextNominal.text.toString().trim().isNotEmpty()) { + binding.editTextNominal.text.toString().toDouble() + } else { + 0.0 + } + + val transaction: Transactions = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = tanggal, + longitude = longitude, + latitude = latitude, + id = transactionId + ) + + if (binding.checkboxUpdateLokasi.isChecked) { + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { + if (it != null) { + latitude = it.latitude.toString() + longitude = it.longitude.toString() + + transaction.latitude = latitude + transaction.longitude = longitude + } + } + } + + showConfirmationDialog("Update", "Apakah Anda yakin ingin memperbarui transaksi ini?") { + transactionsViewModel.upsert(transaction) + + binding.editTextJudul.text = SpannableStringBuilder(transaction.judul) + binding.editTextNominal.text = + SpannableStringBuilder(transaction.nominal.toBigDecimal().toString()) + + if (transaction.longitude.isEmpty() || transaction.latitude.isEmpty()) { + binding.editTextLokasi.text = SpannableStringBuilder("Unavailable") + } else { + val addresses: List<Address> = + Geocoder(requireContext(), Locale.getDefault()).getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + binding.editTextLokasi.text = SpannableStringBuilder(locationName) + } + } + } + + if (transaction.isPemasukan) { + binding.spinnerKategori.setSelection(categories.indexOf(pemasukan)) + } else { + binding.spinnerKategori.setSelection(categories.indexOf(pengeluaran)) + } + Toast.makeText(requireContext(), "Transaksi berhasil diperbarui", Toast.LENGTH_SHORT).show() + } + + + } + + binding.buttonHapus.setOnClickListener { + val transaction: Transactions = Transactions( + judul = "", + nominal = 0.0, + isPemasukan = false, + tanggal = "", + id = transactionId + ) + + showConfirmationDialog("Hapus", "Apakah Anda yakin ingin menghapus transaksi ini?") { + transactionsViewModel.delete(transaction) + findNavController().navigate(com.example.bondoyap.R.id.navigation_transactions) + Toast.makeText(requireContext(), "Transaksi berhasil dihapus", Toast.LENGTH_SHORT).show() + } + } + + return binding.root + } + + private fun showConfirmationDialog(title: String, message: String, action: () -> Unit) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(title) + builder.setMessage(message) + builder.setPositiveButton("Ya") { _, _ -> + action.invoke() + } + builder.setNegativeButton("Tidak") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt new file mode 100644 index 0000000000000000000000000000000000000000..0634b8129e6625beb80b829d449e8f6219f26988 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt @@ -0,0 +1,10 @@ +package com.example.bondoyap.ui.transactions + +import android.app.Application +import com.example.bondoyap.ui.transactions.data.TransactionsRepository +import com.example.bondoyap.ui.transactions.data.TransactionsRoomDatabase + +class TransactionsApplication: Application() { + val database by lazy { TransactionsRoomDatabase.getDatabase(this) } + val repository by lazy { TransactionsRepository(database.transactionsDao()) } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..01ec56df32f5ede55d12ea840944d5e1704bbe31 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt @@ -0,0 +1,74 @@ +package com.example.bondoyap.ui.transactions + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log +import android.widget.Toast +import androidx.core.app.ActivityCompat +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.random.Random + +class TransactionsBroadcastReceiver(private val transactionsViewModel: TransactionsViewModel) : BroadcastReceiver() { + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var latitude: String + private lateinit var longitude: String + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("BroadcastDebug", "Broadcast received in AddTransactionsFragment") + + context ?: return + + val randomJudul = "Random Judul ${Random.nextInt(1000)}" + val randomNominal = Random.nextDouble(1000000000000000.0) + val randomIsPemasukan = Random.nextBoolean() + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val currentDate = dateFormat.format(Date()) + + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { + if(it != null) { + latitude = it.latitude.toString() + longitude = it.longitude.toString() + + val transaction: Transactions = Transactions( + judul = randomJudul, + nominal = randomNominal, + isPemasukan = randomIsPemasukan, + tanggal = currentDate, + longitude = longitude, + latitude = latitude + ) + transactionsViewModel.upsert(transaction) + } + } + + if( + ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + val transaction: Transactions = Transactions( + judul = randomJudul, + nominal = randomNominal, + isPemasukan = randomIsPemasukan, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + Toast.makeText(context.applicationContext, + "Izinkan location permission untuk membuat transaksi random dengan lokasi", + Toast.LENGTH_SHORT).show() + } + + Toast.makeText(context.applicationContext, "Transaksi random telah dibuat", Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt index b8cbc5f6ddde72493ea3dc5d9e81c53015e6557e..f928b30d81367d99044359eca8ad4d5c8754020a 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt @@ -1,42 +1,61 @@ package com.example.bondoyap.ui.transactions +import android.content.pm.PackageManager import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R import com.example.bondoyap.databinding.FragmentTransactionsBinding +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS -class TransactionsFragment : Fragment() { - +class TransactionsFragment: Fragment() { private var _binding: FragmentTransactionsBinding? = null - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private val requestcode: Int = 1 + override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val transactionsViewModel = - ViewModelProvider(this).get(TransactionsViewModel::class.java) - _binding = FragmentTransactionsBinding.inflate(inflater, container, false) - val root: View = binding.root - val textView: TextView = binding.textTransactions - transactionsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + val recyclerView = binding.root.findViewById<RecyclerView>(R.id.recyclerViewTransactions) + val adapter = TransactionsListAdapter(context = requireContext()) + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + transactionsViewModel.allTransactions.observe(viewLifecycleOwner, Observer { transactions -> + transactions?.let { adapter.submitList(it) } + }) + + binding.buttonAddTransaction.setOnClickListener { + findNavController().navigate(R.id.navigation_add_transactions) } - return root + + return binding.root } + override fun onDestroyView() { super.onDestroyView() _binding = null } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c5dce13bf504632c030d4a57aa4b6fc47b3991e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt @@ -0,0 +1,147 @@ +package com.example.bondoyap.ui.transactions + +import android.content.Context +import android.content.Intent +import android.location.Address +import android.location.Geocoder +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat.startActivity +import androidx.navigation.Navigation +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R +import com.example.bondoyap.ui.transactions.data.Transactions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.IOException +import java.util.Locale + +class TransactionsListAdapter(private val context: Context) : + ListAdapter<Transactions, TransactionsViewHolder>(TransactionsDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionsViewHolder { + return TransactionsViewHolder.create(parent, context) + } + + override fun onBindViewHolder(holder: TransactionsViewHolder, position: Int) { + val currentTransaction = getItem(position) + holder.bind(currentTransaction) + } + +} + +class TransactionsViewHolder(itemView: View, private val context: Context) : RecyclerView.ViewHolder(itemView) { + private val cardView: CardView = itemView.findViewById(R.id.cardViewTransaction) + private val transactionTitle: TextView = itemView.findViewById(R.id.transactionTitle) + private val transactionAmount: TextView = itemView.findViewById(R.id.transactionAmount) + private val transactionCategory: TextView = itemView.findViewById(R.id.transactionCategory) + private val transactionDate: TextView = itemView.findViewById(R.id.transactionDate) + private val transactionLocation: TextView = itemView.findViewById(R.id.transactionLocation) + private val geocoder: Geocoder = Geocoder(context, Locale.getDefault()) + + fun bind(transaction: Transactions) { + val maxAmountLength = 12 + val maxTitleLength = 16 + val maxLocationLength = 9 + + val amountText = if (transaction.nominal.toBigDecimal().toString().length > maxAmountLength) { + "IDR " + transaction.nominal.toBigDecimal().toString().substring(0, maxAmountLength) + "..." + } else { + "IDR " + transaction.nominal.toBigDecimal().toString() + } + + val titleText = if (transaction.judul.length > maxTitleLength) { + transaction.judul.substring(0, maxTitleLength) + "..." + } else { + transaction.judul + } + + transactionTitle.text = titleText + transactionAmount.text = amountText + transactionCategory.text = when (transaction.isPemasukan) { + true -> "Pemasukan" + else -> "Pengeluaran" + } + + transactionDate.text = transaction.tanggal + + if (transaction.latitude.isNotEmpty() && transaction.longitude.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { + try { + val addresses: List<Address> = geocoder.getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + transactionLocation.text = if (locationName.length > maxLocationLength) { + locationName.substring(0, maxLocationLength) + "..." + } else { + locationName + } + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + } else { + transactionLocation.text = "Unavailable" + } + + transactionLocation.setOnClickListener { + if (transactionLocation.text != "Unavailable") { + val mapUri = Uri.parse("https://maps.google.com/maps/search/?api=1&query=${transaction.latitude},${transaction.longitude}") + val intent = Intent(Intent.ACTION_VIEW, mapUri) + intent.setPackage("com.google.android.apps.maps") + if (intent.resolveActivity(context.packageManager) != null) { + startActivity(context, intent, null) + } else { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com/maps/search/?api=1&query=${transaction.latitude},${transaction.longitude}")) + if (webIntent.resolveActivity(context.packageManager) != null) { + startActivity(context, webIntent, null) + } else { + Toast.makeText(context, "Tidak ada app yang dapat menghandle maps", Toast.LENGTH_SHORT).show() + } + } + } + } + + cardView.setOnClickListener { + val bundle: Bundle = Bundle() + bundle.putInt("transaction_id", transaction.id) + Navigation.findNavController(itemView).navigate(R.id.navigation_edit_transactions, bundle) + } + } + + companion object { + fun create(parent: ViewGroup, context: Context): TransactionsViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.recyclerview_transactions, parent, false) + return TransactionsViewHolder(view, context) + } + } +} + +class TransactionsDiffCallback : DiffUtil.ItemCallback<Transactions>() { + override fun areItemsTheSame(oldItem: Transactions, newItem: Transactions): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Transactions, newItem: Transactions): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt index 1a790c6f8709dc3c2f42d091b860cde6389501c3..012172e8ce21819fb181c307aa74520ef627be80 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt @@ -1,13 +1,41 @@ package com.example.bondoyap.ui.transactions import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.example.bondoyap.ui.transactions.data.Transactions +import com.example.bondoyap.ui.transactions.data.TransactionsRepository +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.launch -class TransactionsViewModel : ViewModel() { +class TransactionsViewModel( + private val repository: TransactionsRepository +): ViewModel() { + val allTransactions: LiveData<List<Transactions>> = repository.allTransactions.asLiveData() - private val _text = MutableLiveData<String>().apply { - value = "This is Transactions Fragment" + fun upsert(transactions: Transactions) = viewModelScope.launch { + repository.upsert(transactions) + } + fun delete(transactions: Transactions) = viewModelScope.launch { + repository.delete(transactions) + } + suspend fun get(transactionId: Int?): Transactions { + val deferred: Deferred<Transactions> = viewModelScope.async { + repository.get(transactionId) + } + return deferred.await() + } +} + +class TransactionsViewModelFactory(private val repository: TransactionsRepository): ViewModelProvider.Factory { + override fun<T: ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(TransactionsViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return TransactionsViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") } - val text: LiveData<String> = _text } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5bddbd9edcc1d9bc8323ad08c4ae69d6949e37d --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt @@ -0,0 +1,30 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "transactions") +data class Transactions( + @ColumnInfo(name = "judul") + val judul: String, + + @ColumnInfo(name = "nominal") + val nominal: Double, + + @ColumnInfo(name = "is_pemasukan") + val isPemasukan: Boolean, + + @ColumnInfo(name = "tanggal") + val tanggal: String, + + @ColumnInfo(name = "longitude") + var longitude: String = "", + + @ColumnInfo(name = "latitude") + var latitude: String = "", + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + val id: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..d09b3619c20f83a05fc1a292f450f720efc04412 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt @@ -0,0 +1,23 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransactionsDao { + + @Upsert + suspend fun upsertTransaction(transaksi: Transactions) + + @Delete + suspend fun deleteTransaction(transaksi: Transactions) + + @Query("SELECT * FROM transactions") + fun getTransactions(): Flow<List<Transactions>> + + @Query("SELECT * FROM transactions WHERE transactions.id == :transactionsId") + suspend fun getTransactionById(transactionsId: Int?): Transactions +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a5dcda7972ac83f2c201507de6d43d2fe30c68d --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt @@ -0,0 +1,22 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.annotation.WorkerThread +import kotlinx.coroutines.flow.Flow + +class TransactionsRepository(private val transactionsDao: TransactionsDao) { + val allTransactions: Flow<List<Transactions>> = transactionsDao.getTransactions() + + @WorkerThread + suspend fun upsert(transactions: Transactions) { + transactionsDao.upsertTransaction(transactions) + } + + @WorkerThread + suspend fun delete(transactions: Transactions) { + transactionsDao.deleteTransaction(transactions) + } + + suspend fun get(transactionId: Int?): Transactions { + return transactionsDao.getTransactionById(transactionId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..ada79cee07b3f11afee9b2a627791b737ab9c644 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt @@ -0,0 +1,70 @@ +package com.example.bondoyap.ui.transactions.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Database( + entities = [Transactions::class], + version = 3, + exportSchema = false +) +public abstract class TransactionsRoomDatabase: RoomDatabase() { + + abstract fun transactionsDao(): TransactionsDao + + companion object { + @Volatile + private var INSTANCE: TransactionsRoomDatabase? = null + + fun getDatabase(context: Context): TransactionsRoomDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + TransactionsRoomDatabase::class.java, + "transactions_database" + ).addMigrations(MIGRATION_2_3). + build() + INSTANCE = instance + instance + } + } + private val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `transactions_new` " + + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`judul` TEXT NOT NULL, " + + "`nominal` REAL NOT NULL, " + + "`is_pemasukan` INTEGER NOT NULL, " + + "`tanggal` TEXT NOT NULL, " + + "`lokasi` TEXT NOT NULL)") + + db.execSQL("INSERT INTO transactions_new (id, judul, nominal, is_pemasukan) " + + "SELECT id, judul, nominal, is_pemasukan FROM transactions") + + db.execSQL("DROP TABLE transactions") + + db.execSQL("ALTER TABLE transactions_new RENAME TO transactions") + } + } + private val MIGRATION_2_3: Migration = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `transactions_new` " + + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`judul` TEXT NOT NULL, " + + "`nominal` REAL NOT NULL, " + + "`is_pemasukan` INTEGER NOT NULL, " + + "`tanggal` TEXT NOT NULL, " + + "`longitude` TEXT NOT NULL, " + + "`latitude` TEXT NOT NULL)") + + db.execSQL("DROP TABLE transactions") + + db.execSQL("ALTER TABLE transactions_new RENAME TO transactions") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 0000000000000000000000000000000000000000..4304772446917f94787245ca25f0020e4a904ccd --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + + <path + android:fillColor="#FF000000" + android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5s-1.12,2.5 -2.5,2.5z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb0f40356c67383e0760d86a3ad14bcd11220a7d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + + <path + android:fillColor="#000" + android:pathData="M19,13H13v6h-2v-6H5v-2h6V5h2v6h6v2z"/> + +</vector> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b12ccb0da844387368336a0eb6e22e5e60c954c5..bad97d63dc8bac143e499f7ff3a349305c72700f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" - android:layout_height="match_parent" - > + android:layout_height="match_parent"> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/nav_view" diff --git a/app/src/main/res/layout/fragment_add_transactions.xml b/app/src/main/res/layout/fragment_add_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7bc521ef2c2912a1614877a151b223a7d326f98 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_transactions.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_judul" /> + <EditText + android:id="@+id/editText_judul" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="judul" + android:hint="@string/hint_judul" + android:inputType="text" + android:maxLength="150" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_nominal" /> + <EditText + android:id="@+id/editText_nominal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="nominal" + android:hint="@string/hint_nominal" + android:inputType="numberDecimal" + android:maxLength="18" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_kategori" /> + <Spinner + android:id="@+id/spinner_kategori" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" /> + + <Button + android:id="@+id/button_simpan" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/button_simpan" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_transactions.xml b/app/src/main/res/layout/fragment_edit_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea202d0be3584d842892ba196c00dd45967f3216 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_transactions.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_judul" /> + <EditText + android:id="@+id/editText_judul" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="judul" + android:hint="@string/hint_judul" + android:inputType="text" + android:maxLength="150" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_nominal" /> + <EditText + android:id="@+id/editText_nominal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="nominal" + android:hint="@string/hint_nominal" + android:inputType="numberDecimal" + android:maxLength="18" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_kategori" /> + <Spinner + android:id="@+id/spinner_kategori" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_lokasi" /> + <EditText + android:id="@+id/editText_lokasi" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="text|textMultiLine" + android:gravity="top"/> + + <CheckBox android:id="@+id/checkbox_update_lokasi" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Perbarui lokasi" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_hapus" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="8dp" + android:text="@string/button_hapus" + android:backgroundTint="@color/red"/> + + <Button + android:id="@+id/button_update" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/button_update" /> + + </LinearLayout> + + + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_transactions.xml b/app/src/main/res/layout/fragment_transactions.xml index 45fe32c51b6329dc85f34563c32e86ecd2491f52..ad4fddd01c2a89be080305241b2b70399ef56d7a 100644 --- a/app/src/main/res/layout/fragment_transactions.xml +++ b/app/src/main/res/layout/fragment_transactions.xml @@ -1,22 +1,23 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout 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" +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.transactions.TransactionsFragment"> + android:layout_marginTop="16dp"> - <TextView - android:id="@+id/text_transactions" + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerViewTransactions" android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="60dp"/> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/button_addTransaction" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_marginEnd="24dp" + android:layout_marginBottom="72dp" + android:src="@drawable/ic_plus"/> + +</RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/recyclerview_transactions.xml b/app/src/main/res/layout/recyclerview_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..03524715210c6a87729033adc340fe07537e8e47 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_transactions.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/cardViewTransaction" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:cardCornerRadius="8dp" + app:cardElevation="4dp" + android:clickable="true" + android:focusable="true" + app:cardBackgroundColor="#af5eff"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/transactionDate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Tanggal" /> + + <TextView + android:id="@+id/transactionTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="20sp" + android:textStyle="bold" + android:layout_marginTop="5dp" + android:layout_marginBottom="5dp" + android:text="Judul" /> + + <TextView + android:id="@+id/transactionAmount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Nominal" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/transactionCategory" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Kategori" /> + + <TextView + android:id="@+id/transactionLocation" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Lokasi" + android:layout_marginTop="36dp" + app:drawableLeftCompat="@drawable/ic_pin" /> + + </LinearLayout> + + </LinearLayout> + +</androidx.cardview.widget.CardView> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 4412b02a53bd550175ae936468dceed611158b3d..27c15a681a39c24c4ffedbc1e5ff8e7864537988 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -17,6 +17,18 @@ android:label="@string/title_transactions" tools:layout="@layout/fragment_transactions" /> + <fragment + android:id="@+id/navigation_add_transactions" + android:name="com.example.bondoyap.ui.transactions.AddTransactionsFragment" + android:label="@string/title_transactions" + tools:layout="@layout/fragment_add_transactions" /> + + <fragment + android:id="@+id/navigation_edit_transactions" + android:name="com.example.bondoyap.ui.transactions.EditTransactionsFragment" + android:label="@string/title_transactions" + tools:layout="@layout/fragment_edit_transactions" /> + <fragment android:id="@+id/navigation_scanner" android:name="com.example.bondoyap.ui.scanner.ScannerFragment" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 09b8661f01111e613262986d67f2d021709e896c..d59eb366c2f88c41ccacee48df8917b49bf379b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,4 +21,18 @@ <string name="random_transactions">Membuat transaksi random</string> <string name="save_transactions">Simpan daftar transaksi</string> <string name="send_transactions">Kirim daftar transaksi</string> + + <string name="textfield_label_judul">Judul</string> + <string name="textfield_label_nominal">Nominal</string> + <string name="textfield_label_kategori">Kategori</string> + <string name="textfield_label_lokasi">Lokasi</string> + <string name="button_simpan">Simpan</string> + <string name="button_hapus">Hapus</string> + <string name="button_update">Update</string> + <string name="hint_judul">Enter Judul</string> + <string name="hint_nominal">Enter Nominal</string> + <string name="hint_kategori">Enter Kategori</string> + <string name="hint_lokasi">Enter Lokasi</string> + <string name="pemasukan">Pemasukan</string> + <string name="pengeluaran">Pengeluaran</string> </resources> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 31ed43cc99341f5b95f5b15fd54f84d14a585002..96e26d4e6b245ef5a2d5b0bba7d68c7d716a61d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "8.3.0" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } \ No newline at end of file