diff --git a/.gitignore b/.gitignore index aa724b77071afcbd9bb398053e05adaf7ac9405a..5bd175f459a1d5c0ca5b824c2f9bab96ad43d9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +.idea diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 32522c1e7054e664d0b44bf5c384d6e06213b9a5..0897082f7512e48e89310db81b5455d997417505 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> + <component name="GradleMigrationSettings" migrationVersion="1" /> <component name="GradleSettings"> <option name="linkedExternalProjectsSettings"> <GradleProjectSettings> diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cbd33a2f389d524bc4bfef9c52e1f7ab490..8978d23db569daa721cb26dde7923f4c673d1fc9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> diff --git a/README.md b/README.md index 27d666e9d0e1d5cd579810def40c009ba1e70a3e..19b4de1984e2a7683aefb07fa903c942229676b6 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,91 @@ # IF3210-2024-Android-YAP +K2 - Kelompok YAP +- 13521110 Yanuar Sano Nur Rasyid +- 13521112 Rayhan Hanif Maulana Pradana +- 13521173 Dewana Gustavus Haraka Otang -## Getting started +## Deskripsi aplikasi -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +Aplikasi BondoYap merupakan aplikasi manajemen keuangan yang dibuat untuk melakukan pengelolaan transaksi. Fitur yang ada pada aplikasi ini adalah enambahan, pengubahan, dan penghapusan transaksi, daftar transaksi lengkap, scan nota, grafik rangkuman transaksi, ekspor transaksi ke spreadsheet, serta pengiriman file transaksi melalui email. Selain itu terdapat fitur keamanan, seperti deteksi sinyal internet dan service latar belakang untuk memeriksa kevalidan token login. -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +## Library yang digunakan -## Add your files +Aplikasi diimplementasikan dengan bahasa Kotlin. +Berikut adalah library yang digunakan -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +- com.google.android.gms:play-services-location:21.2.0 +- androidx.core:core-ktx:1.8.0 +- androidx.appcompat:appcompat:1.5.1 +- com.google.android.material:material:1.9.0 +- sandroidx.constraintlayout:constraintlayout:2.1.4 +- androidx.lifecycle:lifecycle-livedata-ktx:2.5.1 +- androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1 +- androidx.navigation:navigation-fragment-ktx:2.5.3 +- androidx.navigation:navigation-ui-ktx:2.5.3 +- androidx.legacy:legacy-support-v4:1.0.0 +- androidx.fragment:fragment-ktx:1.5.7 +- com.squareup.retrofit2:retrofit:2.9.0 +- com.squareup.okhttp3:okhttp:4.12.0 +- com.squareup.okhttp3:logging-interceptor +- com.squareup.moshi:moshi:1.15.0 +- com.squareup.moshi:moshi-kotlin:1.15.0 +- com.squareup.retrofit2:converter-moshi:2.9.0 +- androidx.preference:preference:1.2.1 +- io.github.evanrupert:excelkt:1.0.2 +- androidx.room:room-ktx:2.6.1 +- com.github.PhilJay:MPAndroidChart:v3.1.0 +- junit:junit:4.13.2 +- androidx.test.ext:junit:1.1.5 +- androidx.test.espresso:espresso-core:3.5.1 +- androidx.camera:camera-camera2:1.3.2 +- androidx.camera:camera-view:1.3.2 +- androidx.camera:camera-lifecycle:1.3.2 +- androidx.activity:activity-ktx:1.8.2 -``` -cd existing_repo -git remote add origin https://gitlab.informatika.org/yansans/if3210-2024-android-yap.git -git branch -M main -git push -uf origin main -``` +## Screenshot aplikasi (dimasukkan dalam folder screenshot) -## Integrate with your tools +### Login -- [ ] [Set up project integrations](https://gitlab.informatika.org/yansans/if3210-2024-android-yap/-/settings/integrations) + -## Collaborate with your team +### Transaksi -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + -## Test and Deploy + -Use the built-in continuous integration in GitLab. + -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +### Graf -*** + -# Editing this README + -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +### Pengaturan -## Suggestions for a good README + -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +## Pembagian kerja anggota kelompok -## Name -Choose a self-explaining name for your project. +| NIM | Nama | Pekerjaan | +|-----------|-------------------------------|---------------------| +| 13521110 | Yanuar Sano Nur Rasyid | Header-Navbar, Login, Logout, JWT Service, Intent Gmail, Simpan Transaksi, Broadcast Receiver | +| 13521112 | Rayhan Hanif Maulana Pradana | CRUD Transaksi, Daftar Transaksi, Intent Lokasi, Random Transaksi, Graf, Simpan Transaksi | +| 13521173 | Dewana Gustavus Haraka Otang | Scanner, Network Sensing | -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +## Jumlah jam persiapan dan pengerjaan untuk masing-masing anggota -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +- 13521110 Yanuar Sano Nur Rasyid + - Persiapan: 12 jam + - Pengerjaan: 24 jam -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +- 13521112 Rayhan Hanif Maulana Pradana + - Persiapan: 12 jam + - Pengerjaan: 24 jam -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +- 13521173 Dewana Gustavus Haraka Otang + - Persiapan: 18 jam + - Pengerjaan: 18 jam diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fbb759e27fa77be41acfe9d65667fc97689a89e2..50b7aba200fb672329fb4a183e5b5c837d32f3f5 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 { @@ -10,7 +11,7 @@ android { defaultConfig { applicationId = "com.example.bondoyap" minSdk = 29 - targetSdk = 34 + targetSdk = 32 versionCode = 1 versionName = "1.0" @@ -33,19 +34,62 @@ android { buildFeatures { viewBinding = true } + + packaging { + resources.excludes.add("META-INF/DEPENDENCIES") + } } + + dependencies { + implementation("com.google.android.gms:play-services-location:21.2.0") + val roomVersion = "2.6.1" - implementation("androidx.core:core-ktx:1.10.1") - implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.core:core-ktx:1.8.0") + implementation("androidx.appcompat:appcompat:1.5.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") - implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") - implementation("androidx.navigation:navigation-ui-ktx:2.6.0") + + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + + implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") + implementation("androidx.navigation:navigation-ui-ktx:2.5.3") + + implementation("androidx.legacy:legacy-support-v4:1.0.0") + + implementation("androidx.fragment:fragment-ktx:1.5.7") + implementation("androidx.annotation:annotation:1.6.0") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation("com.squareup.moshi:moshi:1.15.0") + implementation("com.squareup.moshi:moshi-kotlin:1.15.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("androidx.preference:preference:1.2.1") + + implementation("io.github.evanrupert:excelkt:1.0.2") + + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("androidx.camera:camera-camera2:1.3.2") + implementation("androidx.camera:camera-view:1.3.2") + implementation("androidx.camera:camera-lifecycle:1.3.2") + + implementation("androidx.activity:activity-ktx:1.8.2") + + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4190e448b161c2f9bcf48d3b2a49d372657c9b1a..4268f2ee25bb1ade5a1792c2218906851a9d5cf4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,37 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + + <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.ACCESS_NETWORK_STATE" /> + + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + + + <queries> + <intent> + <action android:name="android.intent.action.SENDTO" /> + <data android:scheme="mailto" /> + </intent> + <intent> + <action android:name="android.intent.action.SEND" /> + <data android:mimeType="text/plain" /> + </intent> + </queries> + <application + android:name=".ui.transactions.TransactionsApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" @@ -12,16 +42,57 @@ android:supportsRtl="true" android:theme="@style/Theme.BondoYap" tools:targetApi="31"> + + <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/file_path"/> + </provider> + + <service + android:name=".service.jwt.JwtService" + android:enabled="true" + android:exported="true" + android:permission="android.permission.INTERNET"> + </service> + + <receiver + android:name=".ui.transactions.TransactionsBroadcastReceiver" + android:enabled="true" + android:exported="false"> + </receiver> + <activity - android:name=".MainActivity" + android:name=".ui.login.LoginActivity" android:exported="true" - android:label="@string/app_name"> + android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <activity + android:name=".MainActivity" + android:exported="true"> + </activity> + </application> + <queries> + <intent> + <action android:name="android.intent.action.VIEW" /> + <data android:scheme="geo" /> + </intent> + <intent> + <action android:name="android.intent.action.VIEW" /> + <data android:scheme="https" /> + </intent> + </queries> + </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 5bab25b158049e3b1b46f5873f98ce68a716980c..5ac8d1fb73b41a0e5ace944a96de93b533fa7169 100644 --- a/app/src/main/java/com/example/bondoyap/MainActivity.kt +++ b/app/src/main/java/com/example/bondoyap/MainActivity.kt @@ -1,32 +1,100 @@ 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 com.google.android.material.bottomnavigation.BottomNavigationView +import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +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.databinding.ActivityMainBinding +import com.example.bondoyap.service.NetworkObserver +import com.example.bondoyap.service.api.Constants +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +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 +import com.google.android.material.bottomnavigation.BottomNavigationView class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private lateinit var sharedPreferences: SharedPreferences + private lateinit var networkObserver: NetworkObserver + private lateinit var transactionsBroadcastReceiver: TransactionsBroadcastReceiver + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((applicationContext as TransactionsApplication).repository) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + networkObserver = NetworkObserver(applicationContext) + networkObserver.isConnected.observe(this) { isConnected -> + if (isConnected) { + Toast.makeText(this, "Koneksi terhubung", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, "Koneksi terputus", Toast.LENGTH_SHORT).show() + } + } + + if (!isLoggedIn()) { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + } + + val serviceIntent = Intent(this, JwtService::class.java) + this.startService(serviceIntent) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + val navView: BottomNavigationView = binding.navView val navController = findNavController(R.id.nav_host_fragment_activity_main) - // Passing each menu ID as a set of Ids because each - // menu should be considered as top level destinations. - val appBarConfiguration = AppBarConfiguration(setOf( - R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)) + + val appBarConfiguration = AppBarConfiguration( + setOf( + R.id.navigation_settings, R.id.navigation_transactions, R.id.navigation_scanner, + R.id.navigation_graph + ) + ) + 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/LocationManager.kt b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..3196f9aa9dbadf66b1e00ea7082b75c1a8e45cbf --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt @@ -0,0 +1,61 @@ +package com.example.bondoyap.service + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.fragment.app.FragmentActivity +import com.google.android.gms.location.LocationServices + +class LocationManager { + companion object { + fun haveLocationPermission(context: Context): Boolean { + return (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED) + } + + fun askLocationPermission(context: Context, activity: FragmentActivity) { + if (!haveLocationPermission(context)) { + ActivityCompat.requestPermissions( + activity, arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + } + } + + @SuppressLint("MissingPermission") + fun getLocation(context: Context): Location? { + var location: Location? = null + if (haveLocationPermission(context)) { + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(context) + Log.d("LocationManager", "Getting last location") + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + location = it + } + } + } + + if (location == null) { + Log.d("LocationManager", "Location is null") + } else { + Log.d( + "LocationManager", + "Get location at latitude: ${location?.latitude} and longitude: ${location?.longitude}" + ) + } + return location + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/NetworkObserver.kt b/app/src/main/java/com/example/bondoyap/service/NetworkObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..d350eaa65f1d266a1d345290355340ba3c19f8b9 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/NetworkObserver.kt @@ -0,0 +1,28 @@ +package com.example.bondoyap.service + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +class NetworkObserver(context: Context) { + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val _isConnected = MutableLiveData<Boolean>() + val isConnected: LiveData<Boolean> = _isConnected + + init { + connectivityManager.registerDefaultNetworkCallback(object : + ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + _isConnected.postValue(true) + } + + override fun onLost(network: Network) { + _isConnected.postValue(false) + } + }) + } +} diff --git a/app/src/main/java/com/example/bondoyap/service/SessionManager.kt b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a58fbedd688c66addcce7c0d7e46097fd22abd4 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt @@ -0,0 +1,52 @@ +package com.example.bondoyap.service + +import android.content.Context +import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.ui.login.data.model.LoggedInUser +import com.squareup.moshi.Moshi +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +class SessionManager(context: Context) { + + private var prefs: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, + Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = prefs.edit() + + + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) + + private fun getUser(): LoggedInUser? { + val userJson = prefs.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + + fun saveExp(exp: Long){ + editor.putLong("exp", exp) + editor.apply() + } + + fun hasExp(): Boolean{ + return getExp().toInt() != -1 + } + + fun getExp(): Long { + return prefs.getLong("exp", -1) + } + + fun getToken(): String? { + val user = getUser() + return user?.let { + user.token + } + } + + fun logout(){ + editor.clear() + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..02b03eedad67243043d2ec12012358a46c6ca360 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt @@ -0,0 +1,40 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +class ApiClient { + private lateinit var apiService: ApiService + fun getApiService(context: Context): ApiService { + + if (!::apiService.isInitialized) { + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + val retrofit = Retrofit.Builder() + .baseUrl(Constants.BASE_URL) + .client(okhttpClient(context)) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + + apiService = retrofit.create(ApiService::class.java) + } + return apiService + } + + private fun okhttpClient(context: Context): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(AuthInterceptor(context)) + .build() + } + +} + + + diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..803015586a080eda094a614661e240d63a0eebe1 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt @@ -0,0 +1,30 @@ +package com.example.bondoyap.service.api + +import com.example.bondoyap.service.api.data.BillResponse +import com.example.bondoyap.service.api.data.LoginRequest +import com.example.bondoyap.service.api.data.LoginResponse +import com.example.bondoyap.service.api.data.TokenResponse +import okhttp3.MultipartBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface ApiService { + @POST("auth/login") + fun login( + @Body loginRequest: LoginRequest + ): Call<LoginResponse> + + @POST("auth/token") + fun verifyToken( + @Body token: String + ): Call<TokenResponse> + + @POST("bill/upload") + @Multipart + fun getBill( + @Part photoPart : MultipartBody.Part + ) : Call<BillResponse> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..62f933c2fb87585dbdccb4095fd668ea970d4d1c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt @@ -0,0 +1,21 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.example.bondoyap.service.SessionManager +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(context: Context) : Interceptor { + + private val sessionManager = SessionManager(context) + + override fun intercept(chain: Interceptor.Chain): Response { + val requestBuilder = chain.request().newBuilder() + + sessionManager.getToken()?.let { + requestBuilder.addHeader("Authorization", "Bearer $it") + } + + return chain.proceed(requestBuilder.build()) + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..897daedea631dae9a33ff6c0eca3671394eb39df --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt @@ -0,0 +1,7 @@ +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/service/api/data/BillResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..098c5e658bc2c42e7acb89deea7e3ff7514ea188 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class BillResponse( + val items : Items +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd01390409cf0b82f1638f536e0d814e988817be --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt @@ -0,0 +1,9 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Item( + val name : String, + @Json(name = "qty") + val quantity : Int, + val price : Double +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c779b17b1baebb2337f106ad94a51398ea47458 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Items( + val items : List<Item> +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..583c050d0c129816df5e0901d18bdc0a60534c14 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data + +data class LoginRequest( + val email: String, + val password: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca1bb881dc6e0eabc38cb64374fd2eee615d6a52 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class LoginResponse( + val token: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f8a2be82dbdbfd2846355c965f5072680a02efb --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt @@ -0,0 +1,7 @@ +package com.example.bondoyap.service.api.data + +data class TokenResponse( + val nim: String, + val iat: Long, + val exp: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt new file mode 100644 index 0000000000000000000000000000000000000000..39bfd27dd4a6dcd60679e300b6ed963d2e34071f --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt @@ -0,0 +1,115 @@ +package com.example.bondoyap.service.jwt + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.widget.Toast +import com.example.bondoyap.service.SessionManager +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.TokenResponse +import com.example.bondoyap.ui.login.LoginActivity +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class JwtService : Service() { + + private lateinit var handler : Handler + + private lateinit var session: SessionManager + + private val task = object : Runnable { + override fun run() { + Log.d("JwtService", "Current Time: ${System.currentTimeMillis()}") + + session = SessionManager(applicationContext) + val token = session.getToken() + + if (token != null) { + verifyJwt(applicationContext, token) + } + // 10 second + handler.postDelayed(this, 10000) + } + } + + private lateinit var apiClient: ApiClient + + override fun onCreate() { + super.onCreate() + handler = Handler(mainLooper) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + handler.post(task) + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + handler.removeCallbacks(task) + } + + fun verifyJwt(context: Context, token: String){ + if (session.hasExp()) { + checkJwt(session.getExp()) + return + } + apiClient = ApiClient() + apiClient.getApiService(context).verifyToken(token).enqueue(object: Callback<TokenResponse>{ + override fun onFailure(call: Call<TokenResponse>, t: Throwable) { + Log.d("JwtService", "Error Failure") + } + + override fun onResponse(call: Call<TokenResponse>, response: Response<TokenResponse>) { + Log.d("JwtService", "Getting Response") + + val tokenResponse = response.body() + + if (tokenResponse != null){ + Log.d("JwtService", "Updating Exp") + + session.saveExp(tokenResponse.exp) + checkJwt(tokenResponse.exp) + } else { + Log.d("JwtService", "Error response") + Log.d("JwtService", "Error expired?") + handleExpired() + } + + } + }) + } + + fun checkJwt(exp: Long) { + val currentTimestamp = System.currentTimeMillis() / 1000 + + Log.d("JwtService", "exp : ${exp}") + Log.d("JwtService", "currentTime : ${currentTimestamp}") + + if (currentTimestamp >= exp) { + Log.d("JwtService", "JWT is expired") + handleExpired() + } else { + Log.d("JwtService", "JWT is still valid") + } + } + + fun handleExpired(){ + session.logout() + Toast.makeText(applicationContext, "Session Expired! \n Logging out ...", Toast.LENGTH_LONG).show() + + val intent = Intent(applicationContext, LoginActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startActivity(intent) + stopSelf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/bondoyap/ui/dashboard/DashboardFragment.kt deleted file mode 100644 index 9a5de06b9e4315a3cd3e5b048c523f246279b086..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.bondoyap.ui.dashboard - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.example.bondoyap.databinding.FragmentDashboardBinding - -class DashboardFragment : Fragment() { - - private var _binding: FragmentDashboardBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val dashboardViewModel = - ViewModelProvider(this).get(DashboardViewModel::class.java) - - _binding = FragmentDashboardBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textDashboard - dashboardViewModel.text.observe(viewLifecycleOwner) { - textView.text = it - } - return 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/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/dashboard/DashboardViewModel.kt deleted file mode 100644 index bded14e2ddd762f60cfcad02edcf241ab98df308..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.bondoyap.ui.dashboard - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class DashboardViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is dashboard Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/graph/GraphFragment.kt b/app/src/main/java/com/example/bondoyap/ui/graph/GraphFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..fd0e5571f2a3df73ca88da4b0337dbe8b6c09884 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/graph/GraphFragment.kt @@ -0,0 +1,87 @@ +package com.example.bondoyap.ui.graph + +import android.content.res.Configuration +import androidx.fragment.app.viewModels +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import com.example.bondoyap.R +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import com.github.mikephil.charting.charts.PieChart +import com.github.mikephil.charting.components.Legend +import com.github.mikephil.charting.data.PieData +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.data.PieEntry + +class GraphFragment : Fragment() { + + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private lateinit var pieChart: PieChart + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val root = inflater.inflate(R.layout.fragment_graph, container, false) + pieChart = root.findViewById(R.id.pieChart) + + transactionsViewModel.pemasukanCount.observe(viewLifecycleOwner, Observer { pemasukanCount -> + transactionsViewModel.pengeluaranCount.observe(viewLifecycleOwner, Observer { pengeluaranCount -> + val entries = listOf( + PieEntry(pemasukanCount.toFloat(), "Pemasukan"), + PieEntry(pengeluaranCount.toFloat(), "Pengeluaran") + ) + + val colors = mutableListOf<Int>() + colors.add(ContextCompat.getColor(requireContext(), R.color.green)) + colors.add(ContextCompat.getColor(requireContext(), R.color.red)) + + val pieDataSet = PieDataSet(entries, "") + pieDataSet.valueTextSize = 14f + pieDataSet.colors = colors + + val pieData = PieData(pieDataSet) + + pieChart.data = pieData + pieChart.description.isEnabled = false + + val legend = pieChart.legend + val orientation = resources.configuration.orientation + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT + legend.verticalAlignment = Legend.LegendVerticalAlignment.CENTER + } else { + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.CENTER + legend.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM + } + legend.setDrawInside(false) + + if (isDarkTheme()) { + legend.textColor = ContextCompat.getColor(requireContext(), R.color.white) + } else { + legend.textColor = ContextCompat.getColor(requireContext(), R.color.black) + } + + pieChart.invalidate() + }) + }) + + return root + } + + private fun isDarkTheme(): Boolean { + val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return currentNightMode == Configuration.UI_MODE_NIGHT_YES + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/graph/GraphViewModel.kt similarity index 65% rename from app/src/main/java/com/example/bondoyap/ui/home/HomeViewModel.kt rename to app/src/main/java/com/example/bondoyap/ui/graph/GraphViewModel.kt index b26b13b6655cc57d6c1f917b5c87a099bef14416..4135310bb4de0d8826d6148bc33f4fddb1c38517 100644 --- a/app/src/main/java/com/example/bondoyap/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/graph/GraphViewModel.kt @@ -1,13 +1,13 @@ -package com.example.bondoyap.ui.home +package com.example.bondoyap.ui.graph import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -class HomeViewModel : ViewModel() { +class GraphViewModel : ViewModel() { private val _text = MutableLiveData<String>().apply { - value = "This is home Fragment" + value = "This is Graph Fragment" } val text: LiveData<String> = _text } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/home/HomeFragment.kt b/app/src/main/java/com/example/bondoyap/ui/home/HomeFragment.kt deleted file mode 100644 index 986fc6f56b12f3382c42b1b835f0164a0d533e80..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/home/HomeFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.bondoyap.ui.home - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.example.bondoyap.databinding.FragmentHomeBinding - -class HomeFragment : Fragment() { - - private var _binding: FragmentHomeBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val homeViewModel = - ViewModelProvider(this).get(HomeViewModel::class.java) - - _binding = FragmentHomeBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textHome - homeViewModel.text.observe(viewLifecycleOwner) { - textView.text = it - } - return 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/login/LoggedInUserView.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt new file mode 100644 index 0000000000000000000000000000000000000000..106a5999d4bc31f2babf13d5b526b17ff648626e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt @@ -0,0 +1,9 @@ +package com.example.bondoyap.ui.login + +/** + * User details post authentication that is exposed to the UI + */ +data class LoggedInUserView( + val email: String + //... other data fields that may be accessible to the UI +) \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..e64b43e08ad5bb67e76fc8892e9ef61387d0e9ee --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt @@ -0,0 +1,157 @@ +package com.example.bondoyap.ui.login + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.annotation.StringRes +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.MainActivity +import com.example.bondoyap.R +import com.example.bondoyap.databinding.ActivityLoginBinding + +class LoginActivity : AppCompatActivity() { + + private lateinit var loginViewModel: LoginViewModel + private lateinit var binding: ActivityLoginBinding + private lateinit var sharedPreferences: SharedPreferences + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + + + if (isLoggedIn()) { + navigateToMainActivity() + finish() + } else { + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + val emailEditText = binding.email + val passwordEditText = binding.password + val loginButton = binding.login + val loadingProgressBar = binding.loading + + val factory = LoginViewModelFactory(this) + loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java] + + // skip login + + loginViewModel.loginFormState.observe(this@LoginActivity, Observer { + val loginState = it ?: return@Observer + + // disable login button unless both username / password is valid + loginButton.isEnabled = loginState.isDataValid + + if (loginState.emailError != null) { + emailEditText.error = getString(loginState.emailError) + } + if (loginState.passwordError != null) { + passwordEditText.error = getString(loginState.passwordError) + } + }) + + loginViewModel.loginResult.observe(this@LoginActivity, Observer { + val loginResult = it ?: return@Observer + + loadingProgressBar.visibility = View.GONE + if (loginResult.error != null) { + showLoginFailed(loginResult.error) + } + if (loginResult.success != null) { + updateUiWithUser(loginResult.success) + setResult(RESULT_OK) + finish() + } + }) + + emailEditText.afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + passwordEditText.apply { + afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + false + } + + loginButton.setOnClickListener { + loadingProgressBar.visibility = View.VISIBLE + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + } + } + } + + private fun updateUiWithUser(model: LoggedInUserView) { + val welcome = getString(R.string.welcome) + "\n" + model.email + // TODO : initiate successful logged in experience + Toast.makeText( + applicationContext, + welcome, + Toast.LENGTH_SHORT + ).show() + navigateToMainActivity() + } + + private fun showLoginFailed(@StringRes errorString: Int) { + Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show() + } + + private fun navigateToMainActivity() { + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + finish() + } + + private fun isLoggedIn(): Boolean { + return sharedPreferences.getBoolean("isLoggedIn", false) + } +} + +/** + * Extension function to simplify setting an afterTextChanged action to EditText components. + */ +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginFormState.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginFormState.kt new file mode 100644 index 0000000000000000000000000000000000000000..722e212a66f8bf4db2c33b7927f3e582c3a45414 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginFormState.kt @@ -0,0 +1,10 @@ +package com.example.bondoyap.ui.login + +/** + * Data validation state of the login form. + */ +data class LoginFormState( + val emailError: Int? = null, + val passwordError: Int? = null, + val isDataValid: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginResult.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..dabbd6e50e4d9a2469d42148666d95e48cb4fe82 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginResult.kt @@ -0,0 +1,9 @@ +package com.example.bondoyap.ui.login + +/** + * Authentication result : success (user details) or error message. + */ +data class LoginResult( + val success: LoggedInUserView? = null, + val error: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..fedbf7d9cd2c6686bf8dc47cdf25d6cf84422a40 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt @@ -0,0 +1,52 @@ +package com.example.bondoyap.ui.login + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import android.util.Patterns +import androidx.lifecycle.viewModelScope +import com.example.bondoyap.R +import com.example.bondoyap.ui.login.data.LoginRepository +import com.example.bondoyap.ui.login.data.Result +import kotlinx.coroutines.launch + +class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { + + private val _loginForm = MutableLiveData<LoginFormState>() + val loginFormState: LiveData<LoginFormState> = _loginForm + + private val _loginResult = MutableLiveData<LoginResult>() + val loginResult: LiveData<LoginResult> = _loginResult + + fun login(email: String, password: String) { + viewModelScope.launch { + val result = loginRepository.login(email, password) + if (result is Result.Success) { + _loginResult.value = + LoginResult(success = LoggedInUserView(email = result.data.email)) + } else { + _loginResult.value = LoginResult(error = R.string.login_failed) + } + } + } + + fun loginDataChanged(email: String, password: String) { + if (!isEmailValid(email)) { + _loginForm.value = LoginFormState(emailError = R.string.invalid_username) + } + else if (!isPasswordValid(password)) { + _loginForm.value = LoginFormState(passwordError = R.string.invalid_password) + } + else { + _loginForm.value = LoginFormState(isDataValid = true) + } + } + + private fun isEmailValid(email: String): Boolean { + return Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + + private fun isPasswordValid(password: String): Boolean { + return password.isNotEmpty() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b7abfa109b689c808959179ec0729362d3c2a8d --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt @@ -0,0 +1,24 @@ +package com.example.bondoyap.ui.login + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.bondoyap.ui.login.data.LoginDataSource +import com.example.bondoyap.ui.login.data.LoginRepository + +class LoginViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { + return LoginViewModel( + loginRepository = LoginRepository( + dataSource = LoginDataSource(context), + context = context + + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..00f4b562726b80158e1de36da69e93f895b347ed --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt @@ -0,0 +1,58 @@ +package com.example.bondoyap.ui.login.data + +import android.content.Context +import android.util.Log +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.LoginRequest +import com.example.bondoyap.ui.login.data.model.LoggedInUser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + +/** + * Class that handles authentication w/ login credentials and retrieves user information. + */ +class LoginDataSource(private val context: Context) { + + private lateinit var apiClient: ApiClient + + suspend fun login(email: String, password: String): Result<LoggedInUser> { + return try { + + val token = postLogin(email, password) + + val parts = email.split("@") + val nim = parts[0] + + val user = LoggedInUser(nim, email, token) + + Result.Success(user) + } catch (e: Throwable) { + Result.Error(IOException("Error logging in", e)) + } + } + + private suspend fun postLogin(email: String, password: String):String { + + apiClient = ApiClient() + val service = apiClient.getApiService(context) + + return withContext(Dispatchers.IO) { + try { + val loginRequest = LoginRequest(email, password) + val response = service.login(loginRequest).execute() + + if (response.isSuccessful) { + val loginResponse = response.body() + loginResponse?.token ?: throw IOException("Token not received") + } else { + Log.d("LoginDataSource", "${response.code()} ${response.message()}") + throw IOException("${response.code()} ${response.message()}") + } + } catch (e: Exception) { + Log.d("LoginDataSource", "Login failed: ${e.message}") + throw IOException("Login failed: ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d7293786899ffa28dc67352cbcd15d0149e34ab --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt @@ -0,0 +1,56 @@ +package com.example.bondoyap.ui.login.data + +import com.example.bondoyap.ui.login.data.model.LoggedInUser +import android.content.Context +import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +/** + * Class that requests authentication and user information from the remote data source and + * maintains an in-memory cache of login status and user credentials information. + */ + +class LoginRepository(val dataSource: LoginDataSource, context: Context) { + + private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = sharedPreferences.edit() + + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) + + val isLoggedIn: Boolean + get() = sharedPreferences.getBoolean("isLoggedIn", false) + + suspend fun login(email: String, password: String): Result<LoggedInUser> { + val result = dataSource.login(email, password) + + if (result is Result.Success) { + setLoggedInUser(result.data) + } + + return result + } + + fun logout(){ + editor.clear() + editor.apply() + } + + fun getUser(): LoggedInUser? { + val userJson = sharedPreferences.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + + private fun setLoggedInUser(loggedInUser: LoggedInUser) { + val userJson = userAdapter.toJson(loggedInUser) + editor.putString("loggedInUser", userJson).apply() + editor.putBoolean("isLoggedIn", true).apply() + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/Result.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/Result.kt new file mode 100644 index 0000000000000000000000000000000000000000..f2611a06269f80254cbe007bdd982a5625b4b666 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/Result.kt @@ -0,0 +1,18 @@ +package com.example.bondoyap.ui.login.data + +/** + * A generic class that holds a value with its loading status. + * @param <T> + */ +sealed class Result<out T : Any> { + + data class Success<out T : Any>(val data: T) : Result<T>() + data class Error(val exception: Exception) : Result<Nothing>() + + override fun toString(): String { + return when (this) { + is Success<*> -> "Success[data=$data]" + is Error -> "Error[exception=$exception]" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt new file mode 100644 index 0000000000000000000000000000000000000000..78d7e033176d0673dfb6016ebacf1d2c54c256e0 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt @@ -0,0 +1,10 @@ +package com.example.bondoyap.ui.login.data.model + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +data class LoggedInUser( + val userId: String, + val email: String, + val token: String +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsFragment.kt deleted file mode 100644 index adb624033351ccdaa98a6f5c6198cb6c69dde126..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.bondoyap.ui.notifications - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.example.bondoyap.databinding.FragmentNotificationsBinding - -class NotificationsFragment : Fragment() { - - private var _binding: FragmentNotificationsBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val notificationsViewModel = - ViewModelProvider(this).get(NotificationsViewModel::class.java) - - _binding = FragmentNotificationsBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textNotifications - notificationsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it - } - return 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/scanner/ScanResultFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5a5f4828f7929c6a8a4544735d052b0a84323e7 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt @@ -0,0 +1,150 @@ +package com.example.bondoyap.ui.scanner + +import android.annotation.SuppressLint +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.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.bondoyap.databinding.FragmentScanResultBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.service.api.data.Item +import com.example.bondoyap.ui.scanner.listAdapter.ScanResultListAdapter +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +class ScanResultFragment : Fragment() { + // This property is only valid between onCreateView and + // onDestroyView. + private var _binding: FragmentScanResultBinding? = null + private val binding get() = _binding!! + private lateinit var navController: NavController + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentScanResultBinding.inflate(inflater, container, false) + val root: View = binding.root + + navController = findNavController() + val scannerViewModel: ScannerViewModel by activityViewModels() + val items = scannerViewModel.items ?: throw Exception("Scan Result is null") + Log.d("ScanResult", "Result Received: $items") + + val recyclerView = binding.recyclerView + val adapter = ScanResultListAdapter() + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + adapter.submitList(items) + + binding.cancelButton.setOnClickListener { + navController.popBackStack() + } + + binding.saveButton.setOnClickListener { + saveNota(items) + } + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun saveNota(items: List<Item>) { + try { + Log.d("ScanResult", "Saving note: $items") + saveNotaRepository(items) + Toast.makeText(requireContext(), "Nota berhasil disimpan", Toast.LENGTH_SHORT).show() + navController.popBackStack() + } catch (e: Exception) { + Toast.makeText(requireContext(), "Nota gagal disimpan", Toast.LENGTH_SHORT).show() + Log.e("ScanResult", "Saving note failed:", e) + } + } + + private fun saveNotaRepository(items: List<Item>) { + val dateDateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val titleDateFormat = SimpleDateFormat("dd_MM_yyyy_HH_mm_ss", Locale.getDefault()) + val currentTime = Date() + val currentDate = dateDateFormat.format(currentTime) + val title = titleDateFormat.format(currentTime) + + val value = items.sumOf { it.quantity * it.price } + + val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + Log.d("ScanResult", "Getting location") + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("ScanResult", "Saving note on database") + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + } + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..fb1c14b2dc93bc7534455625b9b5949a5e9bc338 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt @@ -0,0 +1,270 @@ +package com.example.bondoyap.ui.scanner + +import android.Manifest +import android.app.Activity +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +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.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.R +import com.example.bondoyap.databinding.FragmentScannerBinding +import com.example.bondoyap.service.NetworkObserver +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.BillResponse +import com.example.bondoyap.service.api.data.Items +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.FileOutputStream + + +class ScannerFragment : Fragment() { + // This property is only valid between onCreateView and + // onDestroyView. + private var _binding: FragmentScannerBinding? = null + private val binding get() = _binding!! + private lateinit var navController: NavController + private lateinit var networkObserver: NetworkObserver + + // Camera + private var isBackCamera = true + private var frozenPreview: Boolean = false + private var cameraImageFile: File? = null + private lateinit var imageCapture: ImageCapture + private lateinit var cameraLauncher: ActivityResultLauncher<String> + + // Gallery + private lateinit var changeImage: ActivityResultLauncher<Intent> + private lateinit var pickImageLauncher: ActivityResultLauncher<String> + + // Upload + private lateinit var cacheDir: File + private lateinit var contentResolver: ContentResolver + private lateinit var apiClient: ApiClient + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentScannerBinding.inflate(inflater, container, false) + val root: View = binding.root + + navController = findNavController() + networkObserver = NetworkObserver(requireContext()) + + cacheDir = requireContext().cacheDir + contentResolver = requireContext().contentResolver + apiClient = ApiClient() + + cameraLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + startCamera() + } + } + + changeImage = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (networkObserver.isConnected.value == false) { + Toast.makeText(requireContext(), "Tidak ada koneksi", Toast.LENGTH_SHORT).show() + } else if (it.resultCode == Activity.RESULT_OK) { + try { + val data = it.data + val imgUri = data?.data ?: throw Exception("Image Uri is null") + Log.d("ImageInput", "Image Selected with URI: $imgUri") + + // Copying image from external directory to cache + val tempFile = File.createTempFile("Gallery_Image", ".jpg", cacheDir) + val inputStream = contentResolver.openInputStream(imgUri) + val outputStream = FileOutputStream(tempFile) + inputStream?.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + + uploadPhoto(tempFile) + + } catch (e: Exception) { + Log.e("ImageInput", "Image Input Failed:", e) + } + } + } + + val pickImageIntent = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI) + pickImageLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + changeImage.launch(pickImageIntent) + } + } + + imageCapture = ImageCapture.Builder().build() + cameraLauncher.launch(Manifest.permission.CAMERA) + + binding.switchCameraButton.setOnClickListener { + toggleCamera() + } + + binding.captureButton.setOnClickListener { + freezePreview() + } + + binding.galleryButton.setOnClickListener { + pickImageLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + binding.uploadButton.isClickable = false + binding.uploadButton.setOnClickListener { + if (networkObserver.isConnected.value == false) { + Toast.makeText(requireContext(), "Tidak ada koneksi", Toast.LENGTH_SHORT).show() + } else { + cameraImageFile?.let { it1 -> uploadPhoto(it1) } + } + } + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder().build().also { mPreview -> + if (!frozenPreview) { + mPreview.setSurfaceProvider(binding.previewView.surfaceProvider) + } else { + mPreview.setSurfaceProvider(null) + } + } + + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + val cameraSelector = if (isBackCamera) { + CameraSelector.DEFAULT_BACK_CAMERA + } else { + CameraSelector.DEFAULT_FRONT_CAMERA + } + + try { + cameraProvider?.unbindAll() + cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture) + + val tempFile = File.createTempFile("Camera_Image", ".jpg", cacheDir) + val outputOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build() + imageCapture.takePicture(outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val uriImg = Uri.fromFile(tempFile) + cameraImageFile = tempFile + Log.d("CameraX", "Image captured: $uriImg") + } + + override fun onError(exception: ImageCaptureException) { + Log.e( + "CameraX", + "Error capturing image: ${exception.message}", + exception + ) + } + } + ) + } catch (e: Exception) { + Log.e("CameraX", "Starting Camera Failed:", e) + } + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun toggleCamera() { + isBackCamera = !isBackCamera + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun freezePreview() { + frozenPreview = !frozenPreview + if (!frozenPreview) { + binding.captureButton.text = "Capture" + binding.uploadButton.isClickable = false + } else { + binding.captureButton.text = "Retake" + binding.uploadButton.isClickable = true + } + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun uploadPhoto(photo: File) { + try { + val progressBar = binding.loading + progressBar.visibility = View.VISIBLE + + val requestFile = photo.asRequestBody("image/*".toMediaTypeOrNull()) + val requestBody = MultipartBody.Part.createFormData("file", photo.name, requestFile) + val apiCall = apiClient.getApiService(requireContext()).getBill(requestBody) + apiCall.enqueue(object : Callback<BillResponse> { + override fun onResponse( + call: Call<BillResponse>, response: Response<BillResponse> + ) { + if (response.isSuccessful) { + progressBar.visibility = View.GONE + + val billResponse = + response.body() ?: throw Exception("Bill Response is Empty") + Log.d("BillUpload", "Server Response: $billResponse") + navigateScanResult(billResponse.items) + } else { + Log.e("BillUpload", "Error: ${response.code()}") + } + } + + override fun onFailure(call: Call<BillResponse>, t: Throwable) { + Log.e("BillUpload", "Failed to upload bill", t) + } + }) + } catch (e: Exception) { + Log.e("BillUpload", "Bill Upload Failed:", e) + } + } + + private fun navigateScanResult(items: Items) { + val scannerViewModel: ScannerViewModel by activityViewModels() + scannerViewModel.items = items.items + navController.navigate(R.id.action_to_scanResultFragment) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt similarity index 51% rename from app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsViewModel.kt rename to app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt index df2228ff7b6bad69e4e58ad6d11e6f7f513466f3..3e659568fd7e9ca0d091f8b24e1c7703777b3dc6 100644 --- a/app/src/main/java/com/example/bondoyap/ui/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt @@ -1,13 +1,16 @@ -package com.example.bondoyap.ui.notifications +package com.example.bondoyap.ui.scanner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoyap.service.api.data.Item -class NotificationsViewModel : ViewModel() { +class ScannerViewModel : ViewModel() { + var items: List<Item>? = null private val _text = MutableLiveData<String>().apply { - value = "This is notifications Fragment" + value = "This is Scanner Fragment" } val text: LiveData<String> = _text + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..15e214aa61d0894a9bc13b086ea6c9845571a7de --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import androidx.recyclerview.widget.DiffUtil +import com.example.bondoyap.service.api.data.Item + +class ItemDiffCallback : DiffUtil.ItemCallback<Item>() { + override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { + val isNameSame = oldItem.name == newItem.name + val isQuantitySame = oldItem.quantity == newItem.quantity + val isPriceSame = oldItem.price == newItem.price + return isNameSame && isQuantitySame && isPriceSame + } + + override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c751de3299b6cb36e794fe7c31cf07999f5943c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt @@ -0,0 +1,29 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R +import com.example.bondoyap.service.api.data.Item + +class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(item: Item) { + val itemName: TextView = itemView.findViewById(R.id.item_name) + val itemQuantity: TextView = itemView.findViewById(R.id.item_quantity) + val itemPrice: TextView = itemView.findViewById(R.id.item_price) + + itemName.text = "Nama: ${item.name}" + itemQuantity.text = "Jumlah: ${item.quantity}" + itemPrice.text = "Harga: ${item.price}" + } + + companion object { + fun create(parent: ViewGroup): ItemViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.recyclerview_scan_result, parent, false) + return ItemViewHolder(view) + } + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d60897087cbdcf1d42dc7263bc9495e249317a7e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.example.bondoyap.service.api.data.Item + +class ScanResultListAdapter : + ListAdapter<Item, ItemViewHolder>(ItemDiffCallback()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder.create(parent) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val currentItem = getItem(position) + holder.bind(currentItem) + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/EmailHelper.kt b/app/src/main/java/com/example/bondoyap/ui/settings/EmailHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a5feeec4e05570ff34de137f4ccc06c21381f51 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/EmailHelper.kt @@ -0,0 +1,26 @@ +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast + +class EmailHelper(private val context: Context) { + + fun sendGmail(recipientEmail: String, subject: String, message: String, attachment: Uri) { + val emailIntent = Intent(Intent.ACTION_SEND ).apply { + type = "application/vnd.ms-excel" + putExtra(Intent.EXTRA_EMAIL, arrayOf(recipientEmail)) + putExtra(Intent.EXTRA_SUBJECT, subject) + putExtra(Intent.EXTRA_TEXT, message) + putExtra(Intent.EXTRA_STREAM, attachment) + setPackage("com.google.android.gm") + } + + if (emailIntent.resolveActivity(context.packageManager) != null) { + // Start the intent + context.startActivity(emailIntent) + } else { + Toast.makeText(context, "Gmail is client installed.", Toast.LENGTH_SHORT).show(); + } + + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..dcf6420c546113097190383b0de42234c21a829c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt @@ -0,0 +1,292 @@ +package com.example.bondoyap.ui.settings + +import EmailHelper +import android.Manifest +import android.app.AlertDialog +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.example.bondoyap.databinding.FragmentSettingsBinding +import com.example.bondoyap.service.NetworkObserver +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS +import com.example.bondoyap.ui.login.LoginActivity +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import java.io.File + + +class SettingsFragment : Fragment() { + + private var _binding: FragmentSettingsBinding? = null + private val binding get() = _binding!! + + private lateinit var settingsViewModel: SettingsViewModel + private lateinit var networkObserver: NetworkObserver + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + private lateinit var exporter: TransactionsExporter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSettingsBinding.inflate(inflater, container, false) + networkObserver = NetworkObserver(requireContext()) + + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + requireActivity(), + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + PERMISSION_REQUEST_CODE + ) + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val factory = context?.let { SettingsViewModelFactory(it) } + if (factory != null) { + settingsViewModel = ViewModelProvider(this, factory)[SettingsViewModel::class.java] + } else { + throw IllegalStateException("Context is null. Cannot create SettingsViewModelFactory.") + } + val textView = binding.textSettings + val logoutButton = binding.logoutButton + val randomButton = binding.randomTransactions + val saveButton = binding.saveTransactions + val sendButton = binding.sendTransactions + val resetButton = binding.resetButton + + settingsViewModel.getUser()?.let { user -> + val loggedInUserText = "Masuk dengan akun:\n ${user.email}" + textView.text = loggedInUserText + } + val appContext = context?.applicationContext + + if (Build.VERSION.SDK_INT >= 30) { + if (!Environment.isExternalStorageManager()) { + Toast.makeText(appContext, "Perbolehkan Akses file ...", Toast.LENGTH_SHORT).show() + val getpermission = Intent() + getpermission.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + startActivity(getpermission) + } + } + + + logoutButton.setOnClickListener { + showConfirmationDialog("Logout", "Apakah Anda yakin logout dari aplikasi?") { + settingsViewModel.getUser() + settingsViewModel.logout() + + val activity = requireActivity() + val intent = Intent(activity, LoginActivity::class.java) + Toast.makeText(appContext, "Logout Sukses!", Toast.LENGTH_SHORT).show() + activity.startActivity(intent) + activity.finish() + } + + } + + randomButton.setOnClickListener { + Toast.makeText(appContext, "Membuat transaksi random ...", Toast.LENGTH_SHORT).show() + + val intent = Intent(ACTION_RANDOMIZE_TRANSACTIONS) + intent.putExtra("message", "Randomize from setting!") + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + + Log.d("BroadcastDebug", "Sending broadcast from SettingsFragment") + } + + exporter = TransactionsExporter(transactionsViewModel, requireContext()) + + saveButton.setOnClickListener { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + Toast.makeText( + appContext, + "Allow storage permission untuk menyimpan transaksi ke file xls/xlsx", + Toast.LENGTH_SHORT + ).show() + } else { + val formats = arrayOf("XLS", "XLSX") + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Pilih Format File") + builder.setItems(formats) { dialog: DialogInterface, which: Int -> + when (which) { + 0 -> { + exporter.exportToXLS() + Toast.makeText( + appContext, + "Penyimpanan xls pada folder Documents berhasil...", + Toast.LENGTH_SHORT + ).show() + } + + 1 -> { + exporter.exportToXLSX() + Toast.makeText( + appContext, + "Penyimpanan xlsx pada folder Documents berhasil...", + Toast.LENGTH_SHORT + ).show() + } + } + dialog.dismiss() + } + builder.create().show() + } + } + + sendButton.setOnClickListener { + if (networkObserver.isConnected.value == false) { + Toast.makeText(requireContext(), "Tidak ada koneksi", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(appContext, "Mengirimkan transaksi ...", Toast.LENGTH_SHORT).show() + + var recipientEmail: String + val subject = "Transaksi Aplikasi Bondoman" + val message = "Halo, Berikut adalah detail transaksi aplikasi Bondoman.\n " + + "File Transaksi terlampir." + + + val formats = arrayOf("XLS", "XLSX") + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Pilih Format File") + builder.setItems(formats) { dialog: DialogInterface, which: Int -> + when (which) { + 0 -> { + Toast.makeText( + appContext, + "Mengirim file xls...", + Toast.LENGTH_SHORT + ).show() + settingsViewModel.getUser()?.let { user -> + recipientEmail = user.email + val attachment = getAttachmentUri("transactions.xls") + Log.d("Attachment", "Attachment URI: $attachment") + context?.let { EmailHelper(it) } + ?.sendGmail(recipientEmail, subject, message, attachment) + } + } + + 1 -> { + Toast.makeText( + appContext, + "Mengirim file xlsx...", + Toast.LENGTH_SHORT + ).show() + settingsViewModel.getUser()?.let { user -> + recipientEmail = user.email + val attachment = getAttachmentUri("transactions.xls") + Log.d("Attachment", "Attachment URI: $attachment") + context?.let { EmailHelper(it) } + ?.sendGmail(recipientEmail, subject, message, attachment) + } + + } + } + dialog.dismiss() + } + builder.create().show() + } + } + + resetButton.setOnClickListener { + Toast.makeText(appContext, "Transaksi di-reset ...", Toast.LENGTH_SHORT).show() + showConfirmationDialog( + "Reset", + "Apakah Anda yakin ingin me-reset data transaksi?" + ) { + transactionsViewModel.deleteAllTransactions() + Toast.makeText( + appContext, + "Transaksi berhasil di-reset ...", + Toast.LENGTH_SHORT + ).show() + } + } + + } + + 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() + } + + companion object { + private const val PERMISSION_REQUEST_CODE = 1001 + } + + private fun getAttachmentUri(fileName: String): Uri { +// val cacheDir = context?.cacheDir +// val fileCache = File(cacheDir, fileName) + val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + val file = File(documentsDir, fileName) + + Log.d("Attachment", "File path: ${file.absolutePath}") + + if (!file.exists()) { + Log.d("Attachment", "Creating file: ${file.absolutePath}") + Toast.makeText(requireContext(),"Menyimpan File di Documents ...", Toast.LENGTH_SHORT).show() + if (fileName.endsWith(".xls")) { + exporter.exportToXLS() + } else if (fileName.endsWith(".xlsx")) { + exporter.exportToXLSX() + } +// Toast.makeText(requireContext(),"File belum tersimpan! Simpan File terlebih dahulu!", Toast.LENGTH_SHORT).show() + } + + Log.d("Attachment", "File path: ${file.absolutePath}") + + return FileProvider.getUriForFile( + requireContext(), + "${requireContext().packageName}.provider", + file + ) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..126d2f8867aa68b32386f7eb25275f64d1044596 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt @@ -0,0 +1,22 @@ +package com.example.bondoyap.ui.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.bondoyap.ui.login.data.LoginRepository +import com.example.bondoyap.ui.login.data.model.LoggedInUser + +class SettingsViewModel(private val loginRepository: LoginRepository) : ViewModel() { + + private val _text = MutableLiveData<String>().apply { + value = "This is Settings Fragment" + } + val text: LiveData<String> = _text + + fun logout(){ + loginRepository.logout() + } + fun getUser(): LoggedInUser? { + return loginRepository.getUser() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae367283563ca18363bc458743d2df24b2c51472 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt @@ -0,0 +1,23 @@ +package com.example.bondoyap.ui.settings + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.bondoyap.ui.login.data.LoginDataSource +import com.example.bondoyap.ui.login.data.LoginRepository + +class SettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { + return SettingsViewModel( + loginRepository = LoginRepository( + dataSource = LoginDataSource(context), + context = context + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt b/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..4c8b5f4a2120633d869431d0fabd8289d0ac2f83 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt @@ -0,0 +1,122 @@ +package com.example.bondoyap.ui.settings + +import android.content.Context +import android.location.Address +import android.location.Geocoder +import android.os.Environment +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.data.Transactions +import io.github.evanrupert.excelkt.Sheet +import io.github.evanrupert.excelkt.workbook +import kotlinx.coroutines.launch +import org.apache.poi.ss.usermodel.FillPatternType +import org.apache.poi.ss.usermodel.IndexedColors +import java.io.File +import java.util.Locale + +class TransactionsExporter(private val transactionsViewModel: TransactionsViewModel, val context: Context) { + fun exportToXLS() { + transactionsViewModel.viewModelScope.launch { + val transactions = transactionsViewModel.getAllTransactionsList() + Log.d("SaveDebug", "xls function") + writeToExcel("transactions.xls", transactions) + } + } + + fun exportToXLSX() { + transactionsViewModel.viewModelScope.launch { + val transactions = transactionsViewModel.getAllTransactionsList() + Log.d("SaveDebug", "xlsx function") + writeToExcel("transactions.xlsx", transactions) + } + } + + private fun writeToExcel(fileName: String, transactions: List<Transactions>) { + val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + val file = createFile(fileName, "documents") + val path = File(documentsDir, fileName) + + Log.d("Save", path.absolutePath) + + val cacheDir = context.cacheDir + val fileCache = createFile(fileName, "cache") + val pathCache = File(cacheDir, fileName) + + + if (file != null) { + workbook { + sheet("Transactions") { + transactionsHeader() + + for (transaction in transactions) { + row { + cell(transaction.tanggal) + cell(if (transaction.isPemasukan) "Pemasukan" else "Pengeluaran") + cell(transaction.nominal) + cell(transaction.judul) + cell(if (transaction.longitude.isEmpty() || transaction.latitude.isEmpty()) { + "Unavailable" + } else { + val addresses: List<Address> = + Geocoder(context, Locale.getDefault()).getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + addresses[0].getAddressLine(0) ?: "Unavailable" + } else { + "Unavailable" + } + }) + } + } + } + }.write(path.absolutePath) + } else { + // Handle case where file creation failed + println("Failed to create file.") + } + } + + fun createFile(fileName: String, directoryType: String): File? { + val targetDirectory = when (directoryType) { + "documents" -> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "cache" -> context.cacheDir + else -> null + } + + if (targetDirectory != null) { + val file = File(targetDirectory, fileName) + try { + if (!file.exists()) { + file.createNewFile() + } + return file + } catch (e: Exception) { + e.printStackTrace() + } + } + return null + } + + private fun Sheet.transactionsHeader() { + val headings = listOf("Tanggal", "Kategori Transaksi", "Nominal Transaksi", "Nama Transaksi", "Lokasi") + + val headingStyle = createCellStyle { + setFont(createFont { + fontName = "IMPACT" + color = IndexedColors.BLACK.index + }) + + fillPattern = FillPatternType.SOLID_FOREGROUND + fillForegroundColor = IndexedColors.YELLOW.index + } + + row(headingStyle) { + headings.forEach { cell(it) } + } + } +} \ No newline at end of file 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..94fe56e0f5ff49dade43ac7fdd1ac3519ee443b1 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt @@ -0,0 +1,136 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.example.bondoyap.databinding.FragmentAddTransactionsBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.ui.transactions.data.Transactions +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) + } + + 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 + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + + 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 + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } else { + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + } + } else { + val transaction = 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..d0b9e61cd7c193ba3e5f41a0b3fb821c180a4094 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt @@ -0,0 +1,236 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.annotation.SuppressLint +import android.app.AlertDialog +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.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.databinding.FragmentEditTransactionsBinding +import com.example.bondoyap.service.LocationManager +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 = "" + 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) + } + } + } + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + binding.checkboxUpdateLokasi.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (!LocationManager.haveLocationPermission(requireContext())) { + binding.checkboxUpdateLokasi.isChecked = false + LocationManager.askLocationPermission(requireContext(), requireActivity()) + } + } + } + + 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( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = tanggal, + longitude = longitude, + latitude = latitude, + id = transactionId + ) + + if (binding.checkboxUpdateLokasi.isChecked) { + @SuppressLint("MissingPermission") + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { loc -> + if (loc != null) { + latitude = loc.latitude.toString() + longitude = loc.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( + 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..4147202391a0f83e8028e29a74bc023392e30a7a --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt @@ -0,0 +1,93 @@ +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 + + private val listJudulTransaksi = listOf( + "Belanja Bulanan", "Gajian", "THR", "Tagihan Listrik", "Tagihan Air", "Tagihan Internet", "Kebutuhan Dapur", "Pakaian", "Elektronik", "Makanan", + "Bahan Bakar", "Cicilan", "Asuransi", "Pajak", "Angsuran Kredit", "Tiket Transportasi", "Tiket Konser", "Biaya Pendidikan", "Sewa Rumah", "Tagihan Kartu Kredit", + "Gaji Bonus", "Infaq", "Zakat", "Donasi", "Uang Saku", "Tabungan", "Investasi", "Liburan", "Rekreasi", "Hadiah", + "Hutang Lunas", "Pinjaman Lunas", "Pensiun", "Bonus Tahunan", "Uang Jajan", "Royalti", "Hadiah Ulang Tahun", "Bayar Utang", "Refund", "Uang Lebaran", + "Uang Jalan", "Uang Makan", "Uang Sakit", "Uang Pemberian", "Uang Saku Anak", "Pensiun Dini", "Komisi", "Tunai Back" + ) + + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("BroadcastDebug", "Broadcast received in AddTransactionsFragment") + + context ?: return + + val randomJudul = listJudulTransaksi[Random.nextInt(listJudulTransaksi.size)] + 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) + } else { + val transaction: Transactions = Transactions( + judul = randomJudul, + nominal = randomNominal, + isPemasukan = randomIsPemasukan, + tanggal = currentDate, + longitude = "", + 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 new file mode 100644 index 0000000000000000000000000000000000000000..f928b30d81367d99044359eca8ad4d5c8754020a --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt @@ -0,0 +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 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() { + private var _binding: FragmentTransactionsBinding? = null + + 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? + ): View { + _binding = FragmentTransactionsBinding.inflate(inflater, container, false) + + 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..2e06e88c972e779c7b3017a534f00cb5f6fdf083 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt @@ -0,0 +1,51 @@ +package com.example.bondoyap.ui.transactions + +import androidx.lifecycle.LiveData +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( + private val repository: TransactionsRepository +): ViewModel() { + val allTransactions: LiveData<List<Transactions>> = repository.allTransactions.asLiveData() + val pemasukanCount: LiveData<Int> = repository.getPemasukanCount().asLiveData() + val pengeluaranCount: LiveData<Int> = repository.getPengeluaranCount().asLiveData() + + 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() + } + + suspend fun getAllTransactionsList(): List<Transactions> { + return repository.getAllTransactionsList() + } + + fun deleteAllTransactions() = viewModelScope.launch { + repository.deleteAllTransactions() + } +} + +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") + } +} \ 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..f02276d8cd7b09c5d5008953cd3602e30006d5b0 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt @@ -0,0 +1,35 @@ +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") + suspend fun getTransactionsList(): List<Transactions> + + @Query("SELECT * FROM transactions WHERE transactions.id == :transactionsId") + suspend fun getTransactionById(transactionsId: Int?): Transactions + + @Query("DELETE FROM transactions") + suspend fun deleteAllTransactions() + + @Query("SELECT COUNT(transactions.id) FROM transactions WHERE transactions.is_pemasukan = 1") + fun getPemasukanCount(): Flow<Int> + + @Query("SELECT COUNT(transactions.id) FROM transactions WHERE transactions.is_pemasukan = 0") + fun getPengeluaranCount(): Flow<Int> +} \ 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..43b83a602b22ef3f4ccfa1e889d357f7dbce1bc0 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt @@ -0,0 +1,39 @@ +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) + } + + suspend fun getAllTransactionsList(): List<Transactions> { + return transactionsDao.getTransactionsList() + } + + @WorkerThread + suspend fun deleteAllTransactions() { + transactionsDao.deleteAllTransactions() + } + + fun getPemasukanCount(): Flow<Int> { + return transactionsDao.getPemasukanCount() + } + + fun getPengeluaranCount(): Flow<Int> { + return transactionsDao.getPengeluaranCount() + } +} \ 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_gear.xml b/app/src/main/res/drawable/ic_gear.xml new file mode 100644 index 0000000000000000000000000000000000000000..2516ac225a837d24c29da2b9a3cacadc91a717f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_gear.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="512dp" + android:height="512dp" + android:viewportWidth="512" + android:viewportHeight="512"> + <path + android:pathData="M204.7,1.1c-12.4,3 -24.1,13.4 -28.2,25 -0.9,2.3 -2.8,11.9 -4.4,21.4 -3.9,23 -7.4,29.6 -18.9,35.2 -4.6,2.2 -6.9,2.7 -12.7,2.7 -6.1,-0.1 -9,-0.9 -23.5,-6.5 -16.4,-6.3 -16.5,-6.3 -27,-6.4 -9.7,-0 -11,0.2 -17,3.1 -3.6,1.7 -8.5,5.1 -11,7.7 -5.3,5.4 -49.3,80.7 -52.3,89.7 -2.6,7.6 -1.9,19.7 1.5,27.2 3.7,7.8 7.3,11.8 21.5,23.4 7,5.8 13.8,11.9 15.1,13.4 8.6,10.9 8.8,26.8 0.3,37.6 -1.1,1.4 -8,7.5 -15.3,13.6 -14.5,11.9 -17.7,15.5 -21.5,23.6 -3.5,7.5 -4.2,19.6 -1.7,27.2 1.1,3 12.2,23.4 24.9,45.3 25,43.4 27.5,46.8 38.5,52.1 6,2.9 7.3,3.1 17,3.1 10.5,-0.1 10.6,-0.1 27,-6.4 14.5,-5.6 17.4,-6.4 23.5,-6.5 11.7,-0.1 21.1,5.8 26.6,16.6 1.3,2.5 3.2,10.8 5,21.3 3.2,19.3 4.5,23.6 8.8,29.9 4.2,6.1 9.5,10.4 17.1,14l6.5,3.1 48.4,0.3c30.6,0.2 50.2,-0.1 53.3,-0.7 12.1,-2.6 21.8,-10.2 27.3,-21.4 2.8,-5.9 4,-10.4 6.4,-24.8 3.2,-19.6 5.4,-25.4 11.7,-30.9 6,-5.2 11.8,-7.4 19.9,-7.3 6.2,-0 9,0.8 23.5,6.4 16.4,6.3 16.5,6.3 27,6.4 9.7,-0 11,-0.2 17,-3.1 10.9,-5.3 13.5,-8.8 38.3,-51.6 12.4,-21.6 23.4,-41.1 24.4,-43.3 1.3,-2.9 1.7,-6.7 1.8,-14 0,-8.7 -0.3,-10.7 -2.6,-15.5 -3.9,-8.1 -7.2,-11.8 -22,-24 -14.3,-11.9 -18,-16.4 -20.4,-24.3 -1.9,-6.3 -1.9,-9.1 0,-15.4 2.4,-8 6.3,-12.7 20.4,-24.3 14.6,-12 18.1,-15.9 22,-24 2.3,-4.8 2.6,-6.8 2.6,-15.5 -0.1,-7.3 -0.5,-11.1 -1.8,-14 -1,-2.2 -12,-21.7 -24.4,-43.3 -24.8,-42.8 -27.4,-46.3 -38.3,-51.6 -6,-2.9 -7.3,-3.1 -17,-3.1 -10.5,0.1 -10.6,0.1 -27,6.4 -14.5,5.6 -17.4,6.4 -23.5,6.5 -11.7,0.1 -21.1,-5.8 -26.6,-16.6 -1.2,-2.4 -3.2,-10.8 -5,-21.1 -1.5,-9.4 -3.6,-19.2 -4.6,-21.9 -3.4,-9.6 -12.9,-19.1 -23,-23.3 -4.5,-1.9 -7.6,-2 -54.3,-2.2 -27.2,-0.1 -51.2,0.3 -53.3,0.8zM298,40.2c0,0.2 1.3,8 3,17.5 4.3,24.8 9.2,35.6 21.9,47.9 14.1,13.8 34.6,21.4 53.1,19.9 9.7,-0.8 12.4,-1.5 30.8,-8.2 8.4,-3.1 15.4,-5.4 15.5,-5.2 0.2,0.2 9.7,16.6 21.1,36.3 16.5,28.7 20.4,36.2 19.4,37.1 -0.7,0.6 -6.4,5.3 -12.5,10.4 -14.4,11.8 -20.2,18.4 -25.2,28.5 -5.6,11.1 -7.4,18.9 -7.4,31.6 0,12.7 1.8,20.5 7.4,31.6 5,10.1 10.8,16.7 25.2,28.5 6.1,5.1 11.8,9.8 12.5,10.4 1,0.9 -3,8.5 -19.4,37.1 -11.4,19.7 -20.9,36.1 -21.1,36.3 -0.1,0.2 -7.1,-2.1 -15.5,-5.2 -18.4,-6.7 -21.1,-7.4 -30.8,-8.2 -18.5,-1.5 -39.1,6.2 -53.2,19.8 -12.5,12.3 -17.6,23.3 -21.8,48 -1.7,9.5 -3,17.3 -3,17.4 0,0.2 -18.9,0.3 -42,0.3 -23.1,-0 -42,-0.1 -42,-0.3 0,-0.1 -1.3,-7.9 -3,-17.4 -4.2,-24.7 -9.1,-35.3 -21.7,-47.8 -14,-13.8 -34.7,-21.5 -53.3,-20 -9.7,0.8 -12.4,1.5 -30.8,8.2 -8.4,3.1 -15.4,5.4 -15.5,5.2 -3.1,-3.9 -41.5,-72.4 -41,-72.9 0.4,-0.4 6.2,-5.2 12.8,-10.7 14,-11.6 19.3,-17.3 24.3,-26.3 6,-10.7 8.4,-20.6 8.4,-34 0,-13.4 -2.4,-23.3 -8.4,-34 -5,-9 -10.3,-14.7 -24.3,-26.3 -6.6,-5.5 -12.4,-10.3 -12.8,-10.7 -0.5,-0.5 37.9,-69 41,-72.9 0.1,-0.2 7.1,2.1 15.5,5.2 20.7,7.6 22.7,8 34.8,8.1 12.5,-0 20.2,-1.8 31.5,-7.4 9.1,-4.5 16,-9.8 22.7,-17.7 9.1,-10.8 13.4,-21.6 16.8,-42.3 1.1,-6.9 2.3,-13.7 2.6,-15.3l0.6,-2.7 41.9,-0c23,-0 41.9,0.1 41.9,0.2z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M243.5,158.1c-14,1.8 -30.4,8.1 -43.4,16.7 -7.3,4.8 -20.6,18.2 -25.6,25.7 -22.8,34.6 -22.8,76.4 0,111 5,7.5 18.3,20.9 25.6,25.7 38.1,25.1 84.8,22.9 119.1,-5.6 30.1,-24.9 42.7,-67.1 31.1,-104.1 -11.1,-35.6 -40.3,-61.8 -76.1,-68.5 -8.2,-1.5 -23,-2 -30.7,-0.9zM273.5,200.1c17.5,5.4 32.9,20.9 38.7,38.9 2.9,9.1 2.9,24.9 0,34 -5.9,18.2 -21,33.3 -39.2,39.1 -9.2,3 -24,3 -33.5,0.1 -18.5,-5.7 -33.8,-20.8 -39.7,-39.2 -2.9,-9.1 -2.9,-24.9 0.1,-34 6.9,-21.7 26.9,-38.5 49.1,-41.3 5.9,-0.8 18.2,0.4 24.5,2.4z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> +</vector> diff --git a/app/src/main/res/drawable/ic_pie.xml b/app/src/main/res/drawable/ic_pie.xml new file mode 100644 index 0000000000000000000000000000000000000000..b876ce73f18d957c7126be8643307997585dc342 --- /dev/null +++ b/app/src/main/res/drawable/ic_pie.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="512dp" + android:height="512dp" + android:viewportWidth="512" + android:viewportHeight="512"> + <path + android:pathData="M271,120.5l0,120.5 120.5,-0 120.5,-0 0,-12.3c0,-62.1 -23.4,-119.2 -66.5,-162.2 -43.1,-43.2 -99.9,-66.5 -162.2,-66.5l-12.3,-0 0,120.5zM325.5,34.1c58.8,11.9 110.8,52.6 137,107.1 9.3,19.5 16.4,43.7 18.1,62.6l0.7,7.2 -90.2,-0 -90.1,-0 0,-90.1 0,-90.2 7.2,0.7c4,0.4 11.8,1.6 17.3,2.7z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M203.5,62.1c-40.9,3.9 -85,21.8 -118,47.9 -37.5,29.7 -66.3,73.4 -78,118.6 -5.4,20.7 -6.9,33.1 -6.9,57.4 0,24.3 1.5,36.7 6.9,57.4 9.7,37.3 31.1,74.4 59,102.1 34.9,34.7 76.3,55.7 127,64.2 14.4,2.4 50.1,2.4 65,-0 33.7,-5.5 62.8,-16.6 90,-34.5 64.6,-42.3 102.5,-113.3 102.5,-192l0,-12.2 -105,-0 -105,-0 0,-105 0,-105 -14.2,0.1c-7.9,0.1 -18.3,0.6 -23.3,1zM211,170.7l0,78.8 -56,-56c-42.4,-42.4 -55.7,-56.2 -54.7,-57 0.7,-0.6 4.1,-3.1 7.4,-5.8 18.3,-14.1 44.8,-27 68.6,-33.3 8.7,-2.3 25.4,-5.2 30.5,-5.3l4.2,-0.1 0,78.7zM141,349.5c-34.9,34.9 -63.7,63.5 -64,63.5 -0.7,-0.1 -12.1,-15.5 -17,-23 -6.6,-10.3 -15.6,-29.4 -19.9,-42.5 -20.8,-62.8 -8.8,-130.3 32.2,-182.3l5,-6.4 63.6,63.6 63.6,63.6 -63.5,63.5zM419.6,308.2c-4.3,46.3 -30.1,95 -67.1,126.6 -72.4,61.9 -176.1,62.9 -251,2.4l-2.9,-2.3 66.9,-66.9 67,-67 93.9,-0 93.9,-0 -0.7,7.2z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> +</vector> 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/drawable/ic_receipt.xml b/app/src/main/res/drawable/ic_receipt.xml new file mode 100644 index 0000000000000000000000000000000000000000..351fe03f072b9ddf26fd6e49e3ea7dc39a786f93 --- /dev/null +++ b/app/src/main/res/drawable/ic_receipt.xml @@ -0,0 +1,26 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="512dp" + android:height="512dp" + android:viewportWidth="512" + android:viewportHeight="512"> + <path + android:pathData="M115.5,44.8c-7.6,2.6 -13.5,6.5 -18.9,12.1 -5.7,6.2 -9.1,13.3 -10.6,22.1 -0.7,4.6 -1,61 -0.8,191l0.3,184.5 2.3,4.2c4.2,8 14.3,12.1 22.8,9.3 2.1,-0.7 6.7,-4.1 10.5,-7.6 11,-10.3 17.6,-10.8 28.1,-1.8 2.9,2.4 7.8,5.7 10.8,7.2 4.9,2.4 6.6,2.7 16,2.7 9.4,-0 11.1,-0.3 16,-2.7 3,-1.5 7.9,-4.8 10.8,-7.2 9.8,-8.4 16.6,-8.4 26.4,-0 8,6.8 14.8,9.7 24.4,10.2 12.3,0.7 19.8,-2.2 30.7,-11.6 8.5,-7.4 15.1,-6.7 26.8,2.7 15.6,12.6 35,12.1 51.5,-1.3 10.7,-8.8 17.3,-8.3 28.5,1.9 7.7,7.1 12.6,9.1 19.3,8.1 5.8,-1 10.2,-4.1 13.4,-9.4l2.7,-4.7 0.3,-184.5c0.2,-127.4 -0.1,-186.5 -0.8,-191 -2.3,-14.2 -13,-28.1 -25.7,-33.2l-5.8,-2.3 -137,-0.2c-128,-0.2 -137.3,-0.1 -142,1.5zM390.3,65.1c4.9,1.3 10.5,6.3 12.8,11.4 1.8,3.8 1.9,12.1 1.9,185.8l0,181.9 -4.6,-4.1c-6.7,-5.9 -14,-8.4 -24.4,-8.4 -10.5,-0 -17.2,2.4 -25,8.9 -11.8,9.9 -19.1,10 -29.8,0.2 -12.7,-11.6 -33.7,-12.8 -46.9,-2.7 -2.3,1.7 -6,4.5 -8.2,6.1 -2.8,2.2 -5.5,3.2 -8.9,3.6 -5.6,0.5 -9.1,-1 -14.9,-6.3 -13.3,-12.3 -35.2,-13.7 -48.5,-3 -8.9,7.2 -11.8,8.8 -16.7,9.3 -5.6,0.5 -9.1,-1 -14.8,-6.4 -2.1,-2 -6.7,-5 -10.3,-6.7 -5.8,-2.9 -7.4,-3.2 -15.5,-3.2 -10.9,0.1 -18,2.5 -24.9,8.6l-4.6,4.1 0,-181.6c0,-162 0.2,-182.1 1.6,-185.4 2,-4.9 6,-9 10.9,-11.3 3.8,-1.8 9.6,-1.9 135.5,-1.9 88.7,-0 132.7,0.4 135.3,1.1z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M154.4,129.9c-4.4,2.7 -6,7.7 -4.1,12.4 3.1,7.2 -5,6.7 105.8,6.7 98.4,-0 99.7,-0 102.2,-2 3.8,-3 5.2,-8 3.3,-12.3 -2.9,-7.1 3.4,-6.7 -105.8,-6.7 -95.2,-0 -98.4,0.1 -101.4,1.9z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M154.4,193.9c-4.4,2.7 -6,7.7 -4.1,12.4 3.1,7.2 -5,6.7 105.8,6.7 98.4,-0 99.7,-0 102.2,-2 3.8,-3 5.2,-8 3.3,-12.3 -2.9,-7.1 3.4,-6.7 -105.8,-6.7 -95.2,-0 -98.4,0.1 -101.4,1.9z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M154.4,257.9c-4.4,2.7 -6,7.7 -4.1,12.4 3.1,7.2 -5,6.7 105.8,6.7 98.4,-0 99.7,-0 102.2,-2 3.8,-3 5.2,-8 3.3,-12.3 -2.9,-7.1 3.4,-6.7 -105.8,-6.7 -95.2,-0 -98.4,0.1 -101.4,1.9z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M154.4,321.9c-4.4,2.7 -6,7.7 -4.1,12.4 3.1,7.2 -5,6.7 105.8,6.7 98.4,-0 99.7,-0 102.2,-2 3.8,-3 5.2,-8 3.3,-12.3 -2.9,-7.1 3.4,-6.7 -105.8,-6.7 -95.2,-0 -98.4,0.1 -101.4,1.9z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> +</vector> diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e776a94f5eeb3f213fab20e79601a7343e6266d --- /dev/null +++ b/app/src/main/res/drawable/ic_scan.xml @@ -0,0 +1,26 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="512dp" + android:height="512dp" + android:viewportWidth="512" + android:viewportHeight="512"> + <path + android:pathData="M68.3,33.5c-17.5,4.8 -31.7,19.5 -35.3,36.8 -0.7,3.5 -1,20.3 -0.8,50.4 0.3,43.8 0.4,45.2 2.4,47.9 3.9,5.3 7.1,6.9 13.4,6.9 6.3,-0 9.5,-1.6 13.4,-6.9 2,-2.7 2.1,-4.3 2.6,-48.6 0.5,-44.3 0.6,-45.9 2.6,-48.6 1.1,-1.5 3.3,-3.7 4.8,-4.8 2.7,-2 4.2,-2.1 52.6,-2.6 48.4,-0.5 49.9,-0.6 52.6,-2.6 5.3,-3.9 6.9,-7.1 6.9,-13.4 0,-6.3 -1.6,-9.5 -6.9,-13.4 -2.7,-2.1 -4,-2.1 -52.9,-2.3 -40.8,-0.2 -51.2,0.1 -55.4,1.2z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M337.5,33.4c-11.8,5.1 -12.4,22.4 -1.1,28.9 1.5,0.9 15.1,1.3 51.8,1.7 48.2,0.5 49.7,0.6 52.4,2.6 1.5,1.1 3.7,3.3 4.8,4.8 2,2.7 2.1,4.3 2.6,48.6 0.5,44.3 0.6,45.9 2.6,48.6 3.9,5.3 7.1,6.9 13.4,6.9 6.3,-0 9.5,-1.6 13.4,-6.9 2,-2.7 2.1,-4.1 2.4,-47.9 0.2,-30.1 -0.1,-46.9 -0.8,-50.4 -3.7,-17.5 -17.9,-32.2 -35.7,-36.8 -8,-2.1 -101.1,-2.2 -105.8,-0.1z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M41.5,241.4c-6.7,2.9 -10.5,10.6 -9.1,18.1 0.9,4.7 5.9,10.2 10.4,11.5 2.6,0.8 68.9,1 215.4,0.8l211.7,-0.3 2.7,-2.1c5.3,-3.9 6.9,-7.1 6.9,-13.4 0,-6.3 -1.6,-9.5 -6.9,-13.4l-2.7,-2.1 -212.7,-0.2c-175.3,-0.2 -213.2,-0 -215.7,1.1z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M41.5,337.4c-3.7,1.7 -7,5.2 -8.4,8.9 -0.7,1.9 -1.1,18.2 -1.1,47.1 0,47.9 0.2,50.2 5.6,60.8 5.9,11.6 19.6,22 32.7,24.8 3.5,0.7 21.5,1 54.4,0.8 47.9,-0.3 49.2,-0.4 51.9,-2.4 5.3,-3.9 6.9,-7.1 6.9,-13.4 0,-6.3 -1.6,-9.5 -6.9,-13.4 -2.7,-2 -4.2,-2.1 -52.6,-2.6 -48.4,-0.5 -49.9,-0.6 -52.6,-2.6 -1.5,-1.1 -3.7,-3.3 -4.8,-4.8 -2,-2.7 -2.1,-4.3 -2.6,-48.6 -0.5,-44.3 -0.6,-45.9 -2.6,-48.6 -1.1,-1.5 -3.2,-3.7 -4.6,-4.7 -3.4,-2.5 -11.3,-3.2 -15.3,-1.3z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> + <path + android:pathData="M458,337.1c-2.9,1.2 -6.7,4.6 -8.3,7.4 -0.9,1.4 -1.4,15.7 -1.7,47.7 -0.5,44.1 -0.6,45.7 -2.6,48.4 -1.1,1.5 -3.3,3.7 -4.8,4.8 -2.7,2 -4.2,2.1 -52.6,2.6 -48.4,0.5 -49.9,0.6 -52.6,2.6 -5.3,3.9 -6.9,7.1 -6.9,13.4 0,6.3 1.6,9.5 6.9,13.4 2.7,2 4,2.1 51.9,2.4 32.9,0.2 50.9,-0.1 54.4,-0.8 17.9,-3.8 33.6,-19.4 37.3,-37.5 0.7,-3.3 1,-21.2 0.8,-50.2 -0.3,-43.8 -0.4,-45.2 -2.4,-47.9 -1.1,-1.5 -3.2,-3.7 -4.6,-4.7 -3.1,-2.3 -11.1,-3.2 -14.8,-1.6z" + android:fillColor="#000000" + android:strokeColor="#00000000"/> +</vector> diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..37aba8c28c124c445dcfff4c0f11adb05d4dc31f --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,30 @@ +<?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" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.bottomnavigation.BottomNavigationView + android:id="@+id/nav_view" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="?android:attr/windowBackground" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:menu="@menu/bottom_nav_menu" /> + + <fragment + android:id="@+id/nav_host_fragment_activity_main" + android:name="androidx.navigation.fragment.NavHostFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/nav_view" + app:navGraph="@navigation/mobile_navigation" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout-land/fragment_graph.xml b/app/src/main/res/layout-land/fragment_graph.xml new file mode 100644 index 0000000000000000000000000000000000000000..e84b522c8852d3f09ae5c6e2f0ba0fedd872aad7 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_graph.xml @@ -0,0 +1,35 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.graph.GraphFragment"> + + <com.github.mikephil.charting.charts.PieChart + android:id="@+id/pieChart" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + + <TextView + android:id="@+id/legendTextView" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:textAlignment="center" + android:layout_marginEnd="16dp" + app:layout_constraintTop_toTopOf="@id/pieChart" + app:layout_constraintBottom_toBottomOf="@id/pieChart" + app:layout_constraintStart_toEndOf="@id/pieChart" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintVertical_bias="0.5" + app:layout_constraintHorizontal_bias="1.0" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf194090807d97abade273046c604fba5498297e --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,71 @@ +<?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" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> + + <EditText + android:id="@+id/email" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="96dp" + android:autofillHints="@string/prompt_email" + android:hint="@string/prompt_email" + android:inputType="textEmailAddress" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <EditText + android:id="@+id/password" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:autofillHints="@string/prompt_password" + android:hint="@string/prompt_password" + android:imeActionLabel="@string/action_sign_in_short" + android:imeOptions="actionDone" + android:inputType="textPassword" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email" /> + + <Button + android:id="@+id/login" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_marginTop="16dp" + android:layout_marginBottom="64dp" + android:enabled="false" + android:text="@string/action_sign_in" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/password" + app:layout_constraintVertical_bias="0.2" /> + + <ProgressBar + android:id="@+id/loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="64dp" + android:layout_marginBottom="64dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/password" + app:layout_constraintStart_toStartOf="@+id/password" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.3" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ 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 06ea6cae22113f243efe317f984f7742418737e8..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:paddingTop="?attr/actionBarSize"> + 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_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml deleted file mode 100644 index 166ab0e9e603c1f230a7b9514d293b963ab2309e..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?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" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".ui.dashboard.DashboardFragment"> - - <TextView - android:id="@+id/text_dashboard" - android:layout_width="match_parent" - 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 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_graph.xml b/app/src/main/res/layout/fragment_graph.xml new file mode 100644 index 0000000000000000000000000000000000000000..9190a62c7ca86d85080f31eeab40675ecd34d1ae --- /dev/null +++ b/app/src/main/res/layout/fragment_graph.xml @@ -0,0 +1,29 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.graph.GraphFragment"> + + <com.github.mikephil.charting.charts.PieChart + android:id="@+id/pieChart" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/legendTextView" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + <TextView + android:id="@+id/legendTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/pieChart" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index f3d9b08ffe6101e25c77c5fae7e28bb5dfa11fbd..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?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" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".ui.home.HomeFragment"> - - <TextView - android:id="@+id/text_home" - android:layout_width="match_parent" - 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 diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml deleted file mode 100644 index d41793572bb3b8347ec4bced74b7bd4a43bed5d4..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_notifications.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?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" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".ui.notifications.NotificationsFragment"> - - <TextView - android:id="@+id/text_notifications" - android:layout_width="match_parent" - 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 diff --git a/app/src/main/res/layout/fragment_scan_result.xml b/app/src/main/res/layout/fragment_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..972c342f9703685caafa1ddf6265ee9e7d1cc188 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_result.xml @@ -0,0 +1,39 @@ +<?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="match_parent" + android:orientation="vertical" + tools:context=".ui.scanner.ScanResultFragment"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <!-- TODO: fix relative to bottom nav bar --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="72dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/cancel_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel_button" /> + + <Button + android:id="@+id/save_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/save_button" /> + + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/fragment_scanner.xml b/app/src/main/res/layout/fragment_scanner.xml new file mode 100644 index 0000000000000000000000000000000000000000..9c7aedd02503c2b8be8fe3c80b6d315da59d9e26 --- /dev/null +++ b/app/src/main/res/layout/fragment_scanner.xml @@ -0,0 +1,63 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.scanner.ScannerFragment"> + + + <androidx.camera.view.PreviewView + android:id="@+id/preview_view" + android:layout_width="match_parent" + android:layout_height="400dp" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/switch_camera_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/switch_camera_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/capture_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/capture_button" + app:layout_constraintStart_toEndOf="@+id/switch_camera_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/gallery_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/gallery_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/capture_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + + <Button + android:id="@+id/upload_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/upload_button" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/capture_button" /> + + <ProgressBar + android:id="@+id/loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/upload_button" + app:layout_constraintHorizontal_bias="0.498" + app:layout_constraintStart_toStartOf="@+id/upload_button" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.775" /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ad6375efde862a889190acda6d565aedd3559b67 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,79 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.settings.SettingsFragment" + android:paddingTop="?attr/actionBarSize" + > + + <TextView + android:id="@+id/text_settings" + android:layout_width="match_parent" + 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_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + <Button + android:id="@+id/random_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/random_transactions" + app:layout_constraintTop_toBottomOf="@+id/text_settings" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/save_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/save_transactions" + app:layout_constraintTop_toBottomOf="@+id/random_transactions" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/send_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/send_transactions" + app:layout_constraintTop_toBottomOf="@+id/save_transactions" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/reset_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/reset" + app:layout_constraintTop_toBottomOf="@+id/send_transactions" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + android:backgroundTint="@color/red"/> + + <Button + android:id="@+id/logout_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/logout" + app:layout_constraintTop_toBottomOf="@+id/reset_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + android:backgroundTint="@color/red"/> + +</androidx.constraintlayout.widget.ConstraintLayout> \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..ad4fddd01c2a89be080305241b2b70399ef56d7a --- /dev/null +++ b/app/src/main/res/layout/fragment_transactions.xml @@ -0,0 +1,23 @@ +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="16dp"> + + <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_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_scan_result.xml b/app/src/main/res/layout/recyclerview_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..c172e9daa24a17d34ac12b0e3de9263d366866c3 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_scan_result.xml @@ -0,0 +1,38 @@ +<?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/card_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:cardBackgroundColor="#af5eff" + app:cardCornerRadius="8dp" + app:cardElevation="4dp"> + + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/item_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_quantity" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_price" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + </LinearLayout> + +</androidx.cardview.widget.CardView> 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/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index fb6d040b9a026ddaa05df0e530ab6f95e6c1f999..a0f1d4fd12f2f8172a99f00773183c8e33490315 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -2,18 +2,23 @@ <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item - android:id="@+id/navigation_home" - android:icon="@drawable/ic_home_black_24dp" - android:title="@string/title_home" /> + android:id="@+id/navigation_transactions" + android:icon="@drawable/ic_receipt" + android:title="@string/title_transactions" /> <item - android:id="@+id/navigation_dashboard" - android:icon="@drawable/ic_dashboard_black_24dp" - android:title="@string/title_dashboard" /> + android:id="@+id/navigation_scanner" + android:icon="@drawable/ic_scan" + android:title="@string/title_scanner" /> <item - android:id="@+id/navigation_notifications" - android:icon="@drawable/ic_notifications_black_24dp" - android:title="@string/title_notifications" /> + android:id="@+id/navigation_graph" + android:icon="@drawable/ic_pie" + android:title="@string/title_graph" /> + + <item + android:id="@+id/navigation_settings" + android:icon="@drawable/ic_gear" + android:title="@string/title_settings" /> </menu> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..036d09bc5fd523323794379703c4a111d1e28a04 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> - <monochrome android:drawable="@drawable/ic_launcher_foreground" /> + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> </adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..036d09bc5fd523323794379703c4a111d1e28a04 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> - <monochrome android:drawable="@drawable/ic_launcher_foreground" /> + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> </adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ed54b070361593fb163c9e1d1860592e25d89e3e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1b43d672103d65381c46eb071bd1416a35b81b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b35925334629c8bba6dc62f069edac0463537576 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-ldpi/ic_launcher.png b/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a69d6f5689fdc214b28bf5fecbf33ab7f2994d Binary files /dev/null and b/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..26e1a02af74c5fd9774f19d7b2822fd0f5908c68 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3a14dadae65b43d5bba7e2fa2038e1e0de1b3f09 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..db1f5f9390945f1e8b0376087c007e563ddde4da Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da081676d42f6c3f78a2c91e7bcedddedb..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc6a68df058b7f7b2a65b63ccb1187b2e1852e7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..991c94a3d9a5a55d6951ebb36e1846a1571bf11d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..41fdbefc963e433891dac0a49ba1ab4ec41fbc7d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b2152740a0a62351f1144cc19e7e322d7bc98ff8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f036a47549d47db79c16788749dca10..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..667709b349b96ceada152a2461ea5eb11f1a13f3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbdd222e1a09b79af898ec63e7ae778d8ec63c4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f5083623b375139afb391af71cc533a7dd37..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..136e87df540dd155c093aaaa0c265356892b946c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6fa1074b79ccd52ef67ac15c5637e85..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..d150fa8feda7d2d53208aae66cf3cebb0843fc05 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..12c7745d046f89e7779d0eb3c4cb7354d995bfad Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37cbc3587421d6889eadd1d91fbf1994d4..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index eae370f28aea7c48b4aa503f298dfb128c5addb5..983df0358bc32b2943927cbc28b36223c5fe0927 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -3,23 +3,59 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mobile_navigation" - app:startDestination="@+id/navigation_home"> + app:startDestination="@+id/navigation_transactions"> + + <!-- <fragment--> + <!-- android:id="@+id/navigation_login"--> + <!-- android:name="com.example.bondoyap.ui.login.LoginActivity"--> + <!-- android:label="@string/title_login"--> + <!-- tools:layout="@layout/fragment_transactions" />--> + + <fragment + android:id="@+id/navigation_transactions" + android:name="com.example.bondoyap.ui.transactions.TransactionsFragment" + 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" + android:label="@string/title_scanner" + tools:layout="@layout/fragment_scanner"> + + <action + android:id="@+id/action_to_scanResultFragment" + app:destination="@id/navigation_scan_result" /> + + </fragment> <fragment - android:id="@+id/navigation_home" - android:name="com.example.bondoyap.ui.home.HomeFragment" - android:label="@string/title_home" - tools:layout="@layout/fragment_home" /> + android:id="@+id/navigation_scan_result" + android:name="com.example.bondoyap.ui.scanner.ScanResultFragment" + android:label="@string/title_scan_result" + tools:layout="@layout/fragment_scan_result" /> <fragment - android:id="@+id/navigation_dashboard" - android:name="com.example.bondoyap.ui.dashboard.DashboardFragment" - android:label="@string/title_dashboard" - tools:layout="@layout/fragment_dashboard" /> + android:id="@+id/navigation_graph" + android:name="com.example.bondoyap.ui.graph.GraphFragment" + android:label="@string/title_graph" + tools:layout="@layout/fragment_graph" /> <fragment - android:id="@+id/navigation_notifications" - android:name="com.example.bondoyap.ui.notifications.NotificationsFragment" - android:label="@string/title_notifications" - tools:layout="@layout/fragment_notifications" /> + android:id="@+id/navigation_settings" + android:name="com.example.bondoyap.ui.settings.SettingsFragment" + android:label="@string/title_settings" + tools:layout="@layout/fragment_settings" /> </navigation> \ No newline at end of file diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e065116699dfedbfa503eb28fe432bf1447dd73 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">200dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ 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 f8c6127d327620c93d2b2d00342a68e97b98a48d..dca02c2d4e6b6474144e731710b897d7d6b9a0a2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="red">#FF0000</color> + <color name="green">#50C878</color> + <color name="ic_launcher_background">#f6bc2b</color> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e00c2dd143c595389b3cf8a32d9dc6aff48ec367..be3e72e0d9dc03c295b60c35b62c23ba8e63c792 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -2,4 +2,7 @@ <!-- Default screen margins, per the Android Design guidelines. --> <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="fragment_horizontal_margin">16dp</dimen> + <dimen name="fragment_vertical_margin">16dp</dimen> </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 aa3cab5198eb63696f1a4d976aaed5921c85a1a4..45465ed1ce1cdf5cd57462a1abec42f9c6cf77c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,50 @@ <resources> <string name="app_name">BondoYap</string> - <string name="title_home">Home</string> - <string name="title_dashboard">Dashboard</string> - <string name="title_notifications">Notifications</string> + <string name="title_transactions">Transaksi</string> + <string name="title_scanner">Scanner</string> + <string name="title_graph">Graph</string> + <string name="title_settings">Pengaturan</string> + <string name="title_scan_result">Scan Result</string> + + <string name="title_login">Login</string> + <string name="prompt_email">Email</string> + <string name="prompt_password">Password</string> + <string name="action_sign_in">Sign in</string> + <string name="action_sign_in_short">Sign in</string> + <string name="welcome">"Selamat Datang!"</string> + <string name="logged_in">Masuk dengan akun:</string> + <string name="invalid_username">Bukan email yang valid</string> + <string name="invalid_password">Password tidak boleh kosong</string> + <string name="login_failed">"Login gagal"</string> + <string name="logout">Logout</string> + <string name="title_activity_login">Login</string> + + <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> + + + <string name="capture_button">Capture</string> + <string name="retake_button">Retake</string> + <string name="switch_camera_button">switch camera</string> + <string name="gallery_button">gallery</string> + <string name="upload_button">upload</string> + + <string name="cancel_button">cancel</string> + <string name="save_button">save</string> + <string name="reset">Reset Transaksi</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/file_path.xml b/app/src/main/res/xml/file_path.xml new file mode 100644 index 0000000000000000000000000000000000000000..9033488b18da2bc4907e27d2daa03711ae6ae8d7 --- /dev/null +++ b/app/src/main/res/xml/file_path.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path + name="cache" + path="." /> + + <external-path + name="external_files" + path="."/> +</paths> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 53f4a67287fcc572dab6ad907bddc40aa4efbfa6..96e26d4e6b245ef5a2d5b0bba7d68c7d716a61d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.2.2" apply false + 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d29e6b673f210fe3bc3fe382c30ac48474d510d2..192890a18e7c40dab55fc8ece7452318a3c78331 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Mar 10 16:49:15 WIB 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/screenshot/graph-landscape.jpg b/screenshot/graph-landscape.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bbe5d37deed8a94b85b31be6f46d317adbda6a9 Binary files /dev/null and b/screenshot/graph-landscape.jpg differ diff --git a/screenshot/graph.jpg b/screenshot/graph.jpg new file mode 100644 index 0000000000000000000000000000000000000000..398334d39f6577093ce42d27b25f2dab545fb9f6 Binary files /dev/null and b/screenshot/graph.jpg differ diff --git a/screenshot/login.jpg b/screenshot/login.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d2de9e07be620c034cf4adc2c529cae36dcc40b Binary files /dev/null and b/screenshot/login.jpg differ diff --git a/screenshot/scanner.jpg b/screenshot/scanner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c5b94ff96925dde6011e268d69b60f27ab004389 Binary files /dev/null and b/screenshot/scanner.jpg differ diff --git a/screenshot/setting.jpg b/screenshot/setting.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85a9899eeb4ff22ab28b64940746f6091e25db6d Binary files /dev/null and b/screenshot/setting.jpg differ diff --git a/screenshot/transaksi-edit.jpg b/screenshot/transaksi-edit.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f4d5f49c2cf2ed6172162eb1602e1c7f7e5db57 Binary files /dev/null and b/screenshot/transaksi-edit.jpg differ diff --git a/screenshot/transaksi-tambah.jpg b/screenshot/transaksi-tambah.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d5820514d1c393581db0b66ecc02196eefb7678 Binary files /dev/null and b/screenshot/transaksi-tambah.jpg differ diff --git a/screenshot/transaksi.jpg b/screenshot/transaksi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..808dcedaba2d6dce6fdc65e9bc4167242b50f87b Binary files /dev/null and b/screenshot/transaksi.jpg differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d01cb6670d2d2430a91c8c0f2a15f4e2e8082de..d616139ee998bbb11fee000a8d87a8f75fc9c2e6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven(url = "https://jitpack.io") } }