diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521af10bcc7fd8cea344038eaaeb78d0ef5..0000000000000000000000000000000000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56e9f285d8cfdc6c270853a5d439021a278..0000000000000000000000000000000000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="CompilerConfiguration"> - <bytecodeTargetLevel target="17" /> - </component> -</project> \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 32522c1e7054e664d0b44bf5c384d6e06213b9a5..0000000000000000000000000000000000000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="GradleSettings"> - <option name="linkedExternalProjectsSettings"> - <GradleProjectSettings> - <option name="externalProjectPath" value="$PROJECT_DIR$" /> - <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" /> - <option name="modules"> - <set> - <option value="$PROJECT_DIR$" /> - <option value="$PROJECT_DIR$/app" /> - </set> - </option> - <option name="resolveExternalAnnotations" value="false" /> - </GradleProjectSettings> - </option> - </component> -</project> \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f973e69a86e6f07f1a1c87f17a31c7235..0000000000000000000000000000000000000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectMigrations"> - <option name="MigrateToGradleLocalJavaHome"> - <set> - <option value="$PROJECT_DIR$" /> - </set> - </option> - </component> -</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6ba496933e103b4a71a5bc5d279f57e859d31f91..0000000000000000000000000000000000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ExternalStorageConfigurationManager" enabled="true" /> - <component name="ProjectRootManager" version="2" languageLevel="JDK_17"> - <output url="file://$PROJECT_DIR$/build/classes" /> - </component> - <component name="ProjectType"> - <option name="id" value="Android" /> - </component> -</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddfbbc029bcab630581847471d7f238ec53..0000000000000000000000000000000000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="" vcs="Git" /> - </component> -</project> \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a81dca6f0e4c492ea48931bc99fefd305f89ca0c..58f2ba955bc75a4d217f1c74c00b0b8418d30fa2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + id("kotlin-android") } android { @@ -44,11 +46,44 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") + + //Pie Chart + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + + //Apache POI for saving file into xls/xlsx + val apachePoiVersion = "5.2.5" + implementation("org.apache.poi:poi:$apachePoiVersion") + implementation("org.apache.poi:poi-ooxml:$apachePoiVersion") + + //Test tools testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + //Room + val roomVersion = "2.6.1" + implementation("androidx.room:room-runtime:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + + //Retrofit + val retrofitVersion = "2.9.0" + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion") + + //CameraX for scan + val cameraXVersion = "1.3.2" + implementation ("androidx.camera:camera-core:${cameraXVersion}") + implementation ("androidx.camera:camera-camera2:${cameraXVersion}") + implementation ("androidx.camera:camera-lifecycle:${cameraXVersion}") +// implementation ("androidx.camera:camera-video:${cameraXVersion}") + + implementation ("androidx.camera:camera-view:${cameraXVersion}") + implementation ("androidx.camera:camera-extensions:${cameraXVersion}") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89dbe27451e4914477fa322a441cbca14af35750..698869905a5e41c4d8a9455e70e52d76340783a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-feature android:name="android.hardware.camera" /> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="28" /> <application + android:name=".BondomanApp" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" @@ -12,16 +18,37 @@ android:supportsRtl="true" android:theme="@style/Theme.Exe_android" tools:targetApi="31"> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="pbd.tubes.exe_android.provider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + <activity android:name=".MainActivity" android:exported="true" - android:label="@string/app_name"> + android:label="@string/app_name" + > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <activity + android:name=".LoginActivity"> + </activity> + + <service android:name=".ui.login.TokenExpirationCheckService" /> + + + </application> </manifest> \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7b554629513161cff09b5b25a7662f18e9908a Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/pbd/tubes/exe_android/BondomanApp.kt b/app/src/main/java/pbd/tubes/exe_android/BondomanApp.kt new file mode 100644 index 0000000000000000000000000000000000000000..899c545f986ee251d725272d334a40cb87a73d6b --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/BondomanApp.kt @@ -0,0 +1,13 @@ +package pbd.tubes.exe_android + +import android.app.Application +import pbd.tubes.exe_android.data.AppContainer +import pbd.tubes.exe_android.data.AppDataContainer + +class BondomanApp : Application() { + lateinit var container: AppContainer + override fun onCreate() { + super.onCreate() + container = AppDataContainer(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt b/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt index c3a88dc6598ba40110510b9caf2eacb496b512ca..90a8658bd6809fee9f42180adad9c682e01800a2 100644 --- a/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt +++ b/app/src/main/java/pbd/tubes/exe_android/MainActivity.kt @@ -1,35 +1,128 @@ package pbd.tubes.exe_android +import android.content.Intent +import android.content.res.Configuration import android.os.Bundle -import com.google.android.material.bottomnavigation.BottomNavigationView +import android.util.Log +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import com.google.android.material.navigation.NavigationBarView import pbd.tubes.exe_android.databinding.ActivityMainBinding +import androidx.lifecycle.Observer +import pbd.tubes.exe_android.ui.NetworkSensing.NetworkLiveData +import pbd.tubes.exe_android.ui.login.TokenExpirationCheckService +import pbd.tubes.exe_android.ui.transactions.AddTransactionFragment class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val TAG_FRAGMENT = "broadcastFragment" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (!isUserLoggedIn()) { + startActivity(Intent(this, LoginActivity::class.java)) + finish() + return + } + + val networkStatusLiveData = NetworkLiveData(this) + networkStatusLiveData.observe(this, Observer { isConnected -> + if (!isConnected) { + showNoInternetPopup() + } + }) + + Log.d("Main", "Ini masuk main") + activateBroadcastReceiverFragment() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + val navController = findNavController(R.id.nav_host_fragment_activity_main) + + // Checking current fragment to show/hide fab + // TODO(checking currentDestination id, this only check "navigation") + val currentDestinationId = navController.currentDestination!!.id + if (currentDestinationId != R.id.navigation_transactions){ + Log.d("MyApp", "current fragment is $currentDestinationId, hiding fab") + binding.addFab.hide() + } else { + Log.d("MyApp", "current fragment is $currentDestinationId, showing fab") + binding.addFab.show() + } - val navView: BottomNavigationView = binding.navView + when (resources.configuration.orientation){ + Configuration.ORIENTATION_LANDSCAPE -> { + binding.navViewRail?.let { + setUpNavBar(it) + } + Log.d("MyApp", "Orientation is LANDSCAPE") + } + Configuration.ORIENTATION_PORTRAIT -> { + binding.navView?.let { + setUpNavBar(it) + } + Log.d("MyApp", "Orientation is PORTRAIT") + } + else -> { + Log.d("MyApp", "What other config?") + } + } + binding.addFab.setOnClickListener { + navController.navigate(R.id.navigation_add_transaction) + } + } + private fun setUpNavBar( + navigationBarView: NavigationBarView + ){ 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 - ) + R.id.navigation_transactions, R.id.navigation_scan, R.id.navigation_chart, R.id.navigation_settings + ), +// fallbackOnNavigateUpListener = {navController.navigateUp()} //TODO(navigate back) ) setupActionBarWithNavController(navController, appBarConfiguration) - navView.setupWithNavController(navController) + navigationBarView.setupWithNavController(navController) + } + override fun onPause() { + super.onPause() + val serviceIntent = Intent(this, TokenExpirationCheckService::class.java) + stopService(serviceIntent) + + } + override fun onResume() { + super.onResume() + val serviceIntent = Intent(this, TokenExpirationCheckService::class.java) + startService(serviceIntent) } + + private fun isUserLoggedIn(): Boolean { + val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) + return sharedPreferences.contains("token") + } + + private fun showNoInternetPopup() { + AlertDialog.Builder(this) + .setTitle("No Internet Connection") + .setMessage("Please check your internet connection and try again.") + .setPositiveButton("OK", null) + .show() + } + + private fun activateBroadcastReceiverFragment() { + // Check if the fragment is already added + if (supportFragmentManager.findFragmentByTag(TAG_FRAGMENT) == null) { + // If not added, add the fragment dynamically + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.add(AddTransactionFragment(), TAG_FRAGMENT) + fragmentTransaction.commit() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/AppContainer.kt b/app/src/main/java/pbd/tubes/exe_android/data/AppContainer.kt new file mode 100644 index 0000000000000000000000000000000000000000..473b6e983048d9318a817e340c03c4972088ee95 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/AppContainer.kt @@ -0,0 +1,25 @@ +package pbd.tubes.exe_android.data + +import android.content.Context +import pbd.tubes.exe_android.data.database.OfflineTransactionsRepository +import pbd.tubes.exe_android.data.database.TransactionDatabase +import pbd.tubes.exe_android.data.database.TransactionsRepository + +/** + * App container for Dependency injection. + */ +interface AppContainer { + val transactionRepository: TransactionsRepository +} + +/** + * [AppContainer] implementation that provides instance of [OfflineTransactionsRepository] + */ +class AppDataContainer(private val context: Context) : AppContainer { + /** + * Implementation for [TransactionsRepository] + */ + override val transactionRepository: TransactionsRepository by lazy { + OfflineTransactionsRepository(TransactionDatabase.getDatabase(context).transactionDao()) + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt b/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..3bfada40dc81274885f010b0d44321593a11b125 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/TokenResponse.kt @@ -0,0 +1,5 @@ +package pbd.tubes.exe_android.data + +data class TokenResponse ( + val exp: Long +) diff --git a/app/src/main/java/pbd/tubes/exe_android/data/api/ApiService.kt b/app/src/main/java/pbd/tubes/exe_android/data/api/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..0651ce4c5692a5fef9749a5379ee6230df315603 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/api/ApiService.kt @@ -0,0 +1,31 @@ +package pbd.tubes.exe_android.data.api + +import okhttp3.MultipartBody +import pbd.tubes.exe_android.data.TokenResponse +import pbd.tubes.exe_android.data.api.login.LoginRequest +import pbd.tubes.exe_android.data.api.login.LoginResponse +import pbd.tubes.exe_android.data.api.upload.UploadResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface ApiService { + @POST("/api/auth/login") + suspend fun login( + @Body credentials: LoginRequest + ): Response<LoginResponse> + + @Multipart + @POST("/api/bill/upload") + suspend fun uploadImage( + @Header("Authorization") authorization : String, + @Part imageFile : MultipartBody.Part + ): Response<UploadResponse> + + @POST("/api/auth/token") + suspend fun decodeToken( + @Header("Authorization") authorization: String): Response<TokenResponse> +} diff --git a/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginRequest.kt b/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3f9fa94bda16dcaa7ad68977278d6bfeefc2275a --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginRequest.kt @@ -0,0 +1,6 @@ +package pbd.tubes.exe_android.data.api.login + +data class LoginRequest( + val email: String, + val password: String +) diff --git a/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginResponse.kt b/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..71ee5e086c99113cb08b8f0607398fbab94b3eae --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/api/login/LoginResponse.kt @@ -0,0 +1,5 @@ +package pbd.tubes.exe_android.data.api.login + +data class LoginResponse( + val token: String +) diff --git a/app/src/main/java/pbd/tubes/exe_android/data/api/upload/UploadResponse.kt b/app/src/main/java/pbd/tubes/exe_android/data/api/upload/UploadResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..8093eae2eeedc244efd275cf66cdd05dc5c3b36a --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/api/upload/UploadResponse.kt @@ -0,0 +1,31 @@ +package pbd.tubes.exe_android.data.api.upload + +import pbd.tubes.exe_android.models.Transaction +import pbd.tubes.exe_android.models.TransactionCategory +import java.time.LocalDate +import java.time.ZoneId + +data class UploadResponse( + val items : Items +) +data class Items( + val items : List<Item> +) +data class Item( + val name : String, + val qty : Int, + val price : Double +) + +fun Item.toTransaction() : Transaction { + return ( + Transaction( + id = 0, + name = this.name, + category = TransactionCategory.PEMBELIAN, + date = LocalDate.now(ZoneId.systemDefault()), + lokasi = "", //TODO(Lokasi dengan maps) + nominal = this.qty * this.price + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/OfflineTransactionsRepository.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/OfflineTransactionsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d4486a1baec16d5e43bbc359f33e42280abe64c --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/OfflineTransactionsRepository.kt @@ -0,0 +1,27 @@ +package pbd.tubes.exe_android.data.database + +import kotlinx.coroutines.flow.Flow +import pbd.tubes.exe_android.data.database.helper.CategoryNominal +import pbd.tubes.exe_android.models.Transaction +import java.time.LocalDate + +class OfflineTransactionsRepository (private val transactionDao: TransactionDao) : + TransactionsRepository { + override fun getAllTransactionsStream(): Flow<List<Transaction>> = transactionDao.getAllTransactions() + + override fun getTransactionStream(id: Int): Flow<Transaction?> = transactionDao.getTransaction(id) + + override suspend fun insertTransaction(transaction: Transaction) = transactionDao.insert(transaction) + + override suspend fun deleteTransaction(transaction: Transaction) = transactionDao.delete(transaction) + + override suspend fun updateTransaction(transaction: Transaction) =transactionDao.update(transaction) + + override suspend fun getAllTransactionsByRecentStream(): Flow<List<Transaction>> = transactionDao.getAllTransactionsByRecent() + + override suspend fun getTransactionsAtDateStream(date: LocalDate): Flow<List<Transaction>> = transactionDao.getTransactionsByDate(date) + + override suspend fun getTransactionsAtLocationStream(location: String): Flow<List<Transaction>> = transactionDao.getTransactionsByLocation(location) + + override suspend fun getTransactionNominalsByCategoryStream(): Flow<List<CategoryNominal>> = transactionDao.getTransactionNominalsGroupByCategory() +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDao.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..3f0de38a6856169c0271e0c00a116dd3ee715bbb --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDao.kt @@ -0,0 +1,41 @@ +package pbd.tubes.exe_android.data.database +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import pbd.tubes.exe_android.data.database.helper.CategoryNominal +import pbd.tubes.exe_android.models.Transaction +import java.time.LocalDate + +@Dao +interface TransactionDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(transaction: Transaction) + + @Update + suspend fun update(transaction: Transaction) + + @Delete + suspend fun delete(transaction: Transaction) + + @Query("SELECT * from transactions WHERE id = :id") + fun getTransaction(id: Int): Flow<Transaction> + + @Query("SELECT * from transactions") + fun getAllTransactions(): Flow<List<Transaction>> + + @Query("SELECT * from transactions ORDER BY id DESC") + fun getAllTransactionsByRecent(): Flow<List<Transaction>> + + @Query("SELECT * from transactions WHERE date = :date") + fun getTransactionsByDate(date: LocalDate): Flow<List<Transaction>> + + @Query("SELECT * FROM transactions WHERE lokasi = :location") + fun getTransactionsByLocation(location: String): Flow<List<Transaction>> + + @Query("SELECT category,SUM(nominal) FROM transactions GROUP BY category") + fun getTransactionNominalsGroupByCategory(): Flow<List<CategoryNominal>> +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDatabase.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd64dac2390a91a0c9a05e2cf26d06586a805182 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionDatabase.kt @@ -0,0 +1,29 @@ +package pbd.tubes.exe_android.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import pbd.tubes.exe_android.data.database.helper.DateConverter +import pbd.tubes.exe_android.models.Transaction + +@Database(entities = [Transaction::class], version = 3, exportSchema = false) +@TypeConverters(DateConverter::class) +abstract class TransactionDatabase : RoomDatabase() { + abstract fun transactionDao() : TransactionDao + + companion object { + @Volatile + private var Instance : TransactionDatabase? = null + + fun getDatabase(context: Context): TransactionDatabase { + return Instance ?: synchronized(this){ + Room.databaseBuilder(context, TransactionDatabase::class.java, "transaction_database") + .fallbackToDestructiveMigration() + .build() + .also{ Instance = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionsRepository.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..47e457eeef1883e428be147bb5b82c5d5e50fe94 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/TransactionsRepository.kt @@ -0,0 +1,40 @@ +package pbd.tubes.exe_android.data.database +import kotlinx.coroutines.flow.Flow +import pbd.tubes.exe_android.data.database.helper.CategoryNominal +import pbd.tubes.exe_android.models.Transaction +import java.time.LocalDate + +/** + * Repository that provides insert, update, delete, and retrieve of [Transaction] from a given data source. + */ +interface TransactionsRepository { + /** + * Retrieve all the transactions from the given data source. + */ + fun getAllTransactionsStream(): Flow<List<Transaction>> + + /** + * Retrieve an Transaction from the given data source that matches with the [id]. + */ + fun getTransactionStream(id: Int): Flow<Transaction?> + + /** + * Insert Transaction in the data source + */ + suspend fun insertTransaction(transaction: Transaction) + + /** + * Delete Transaction from the data source + */ + suspend fun deleteTransaction(transaction: Transaction) + + /** + * Update Transaction in the data source + */ + suspend fun updateTransaction(transaction: Transaction) + suspend fun getAllTransactionsByRecentStream(): Flow<List<Transaction>> + + suspend fun getTransactionsAtDateStream(date: LocalDate): Flow<List<Transaction>> + suspend fun getTransactionsAtLocationStream(location: String): Flow<List<Transaction>> + suspend fun getTransactionNominalsByCategoryStream(): Flow<List<CategoryNominal>> +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/helper/CategoryNominal.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/helper/CategoryNominal.kt new file mode 100644 index 0000000000000000000000000000000000000000..234cda4c913275e1db245e979ce8225794cadc4b --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/helper/CategoryNominal.kt @@ -0,0 +1,20 @@ +package pbd.tubes.exe_android.data.database.helper + +import androidx.room.ColumnInfo +import pbd.tubes.exe_android.models.TransactionCategory +import java.util.EnumMap + +data class CategoryNominal( + @ColumnInfo("category") + val category: TransactionCategory, + @ColumnInfo("SUM(nominal)") + val nominal: Double, +) + +fun List<CategoryNominal>.toCategoryNominalMap(): EnumMap<TransactionCategory, Double> { + val ret = EnumMap<TransactionCategory, Double>(TransactionCategory::class.java) + this.forEach { + ret[it.category] = it.nominal + } + return ret +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/data/database/helper/DateConverter.kt b/app/src/main/java/pbd/tubes/exe_android/data/database/helper/DateConverter.kt new file mode 100644 index 0000000000000000000000000000000000000000..786b48c46c4443070574b694fe39c790ae099846 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/data/database/helper/DateConverter.kt @@ -0,0 +1,17 @@ +package pbd.tubes.exe_android.data.database.helper + +import androidx.room.TypeConverter +import java.time.LocalDate + +class DateConverter { + @TypeConverter + fun fromDate(date: LocalDate): Long { + return date.toEpochDay() + } + + @TypeConverter + fun toDate(epochDay: Long): LocalDate { + return LocalDate.ofEpochDay(epochDay) + } + +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/models/Transaction.kt b/app/src/main/java/pbd/tubes/exe_android/models/Transaction.kt new file mode 100644 index 0000000000000000000000000000000000000000..03a6733454457852ed647a3b3fec3dfe49feebea --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/models/Transaction.kt @@ -0,0 +1,19 @@ +package pbd.tubes.exe_android.models +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDate + +@Entity(tableName = "transactions") +data class Transaction ( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val name: String, + val nominal: Double, + val category: TransactionCategory, + val lokasi: String, //TODO(Lokasi dari lokasi device) + val date: LocalDate, +) + +enum class TransactionCategory { + PEMBELIAN, PEMASUKAN +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt b/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt new file mode 100644 index 0000000000000000000000000000000000000000..6ea0da0a580b52a76896493a604ca8fe4bc4697e --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/NetworkSensing/NetworkLiveData.kt @@ -0,0 +1,42 @@ +package pbd.tubes.exe_android.ui.NetworkSensing + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import androidx.lifecycle.LiveData + +class NetworkLiveData(private val context: Context) : LiveData<Boolean>() { + + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + postValue(true) + } + + override fun onLost(network: Network) { + postValue(false) + } + } + + override fun onActive() { + super.onActive() + registerNetworkCallback() + } + + override fun onInactive() { + super.onInactive() + unregisterNetworkCallback() + } + + private fun registerNetworkCallback() { + val builder = NetworkRequest.Builder() + connectivityManager.registerNetworkCallback(builder.build(), networkCallback) + } + + private fun unregisterNetworkCallback() { + connectivityManager.unregisterNetworkCallback(networkCallback) + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..691ed45b47d16e1c0934c5943286228ba534a998 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartFragment.kt @@ -0,0 +1,84 @@ +package pbd.tubes.exe_android.ui.chart + +import android.graphics.Color +import android.graphics.Typeface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.github.mikephil.charting.charts.PieChart +import com.github.mikephil.charting.data.PieData +import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.formatter.DefaultValueFormatter +import pbd.tubes.exe_android.R +import pbd.tubes.exe_android.databinding.FragmentChartBinding +import pbd.tubes.exe_android.models.TransactionCategory + +class ChartFragment : Fragment() { + + private var _binding: FragmentChartBinding? = null + private val viewModel : ChartViewModel by viewModels (factoryProducer = {ChartViewModel.Factory}) + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentChartBinding.inflate(inflater, container, false) + val root: View = binding.root + + val pieChart: PieChart = binding.pieChart +// pieChart.setUsePercentValues(true) + pieChart.dragDecelerationFrictionCoef = 0.95f + pieChart.isDrawHoleEnabled = true + pieChart.setEntryLabelColor(Color.BLACK) + pieChart.setEntryLabelTextSize(12f) + pieChart.setTransparentCircleAlpha(110) + pieChart.setTransparentCircleColor(Color.WHITE) + pieChart.description = null + + //TODO(Specify legend) + viewModel.fetchTotalByCategory() + viewModel.nominalSumByCategory?.observe(viewLifecycleOwner){ + val dataArrayList = viewModel.dataToArrayList() + val colorsArrayList = ArrayList<Int>() + + for (entry in dataArrayList) { + val color = when (entry.label) { + TransactionCategory.PEMBELIAN.toString() -> + ContextCompat.getColor(requireContext(), R.color.pembelian_color) + TransactionCategory.PEMASUKAN.toString() -> + ContextCompat.getColor(requireContext(), R.color.pemasukan_color) + else -> Color.BLACK // Default color + } + entry.label = entry.label.lowercase().replaceFirstChar { it.uppercase() } + colorsArrayList.add(color) + } + val dataSet = PieDataSet(dataArrayList, "Transaksi") + dataSet.colors = colorsArrayList + val data = PieData(dataSet) + + data.setValueFormatter(DefaultValueFormatter(2)) + data.setValueTextSize(15f) + data.setValueTypeface(Typeface.DEFAULT_BOLD) + data.setValueTextColor(Color.BLACK) + pieChart.centerText = String.format("Total: %.2f", viewModel.totalTransaksi()) + + pieChart.setData(data) + pieChart.invalidate() + } + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..34cd601cd5a9c2357bf221b48dc01af41258d410 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/chart/ChartViewModel.kt @@ -0,0 +1,75 @@ +package pbd.tubes.exe_android.ui.chart + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import com.github.mikephil.charting.data.PieEntry +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.database.TransactionsRepository +import pbd.tubes.exe_android.data.database.helper.toCategoryNominalMap +import pbd.tubes.exe_android.models.TransactionCategory +import java.util.EnumMap + +class ChartViewModel( + private val transactionsRepository: TransactionsRepository +) : ViewModel() { + +// private var _chart = MutableLiveData<>() +// val chart: LiveData<> = _chart + + var nominalSumByCategory : LiveData<EnumMap<TransactionCategory, Double>>? = null + fun fetchTotalByCategory() { + viewModelScope.launch { + nominalSumByCategory = transactionsRepository + .getTransactionNominalsByCategoryStream() + .map { + it.toCategoryNominalMap() + }.asLiveData() + } + } + fun totalTransaksi(): Double { + var sum = 0.0 + for (value in nominalSumByCategory!!.value!!.values) { + sum += value + } + return sum + } + + fun dataToArrayList() : ArrayList<PieEntry> { + val data = ArrayList<PieEntry>() + for (i in nominalSumByCategory!!.value!!.keys){ + data.add( + PieEntry( + nominalSumByCategory!!.value!![i]!!.toFloat(), + i.toString() + ) + ) + } + return data + } + + + companion object { + class ChartViewModelFactory( + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras + ): T { + val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) + // val savedStateHandle = extras.createSavedStateHandle() + return ChartViewModel( + (application as BondomanApp).container.transactionRepository + // savedStateHandle + ) as T + } + } + val Factory: ViewModelProvider.Factory = ChartViewModelFactory() + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/dashboard/DashboardFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/dashboard/DashboardFragment.kt deleted file mode 100644 index ebfc8bdfe88f39b0a5ae4826579dd755b92fecc3..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package pbd.tubes.exe_android.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 pbd.tubes.exe_android.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/pbd/tubes/exe_android/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/dashboard/DashboardViewModel.kt deleted file mode 100644 index 7e5f2a18ca08e30fd58bbef3ca8ffd4c1abcd5b7..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pbd.tubes.exe_android.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/pbd/tubes/exe_android/ui/home/HomeFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/home/HomeFragment.kt deleted file mode 100644 index f5fb5db20df2ca88a7a3d2662c5a98714c62a111..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/home/HomeFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package pbd.tubes.exe_android.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 pbd.tubes.exe_android.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/pbd/tubes/exe_android/ui/home/HomeViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/home/HomeViewModel.kt deleted file mode 100644 index 7adbeb5fec7474d816c69e5233b021d03c7fee3f..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/home/HomeViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pbd.tubes.exe_android.ui.home - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class HomeViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is home Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt b/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c899832b1cb996acbfb35b4ebe92d5f568f23b2b --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/login/LoginActivity.kt @@ -0,0 +1,126 @@ +package pbd.tubes.exe_android + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import pbd.tubes.exe_android.data.api.ApiService +import pbd.tubes.exe_android.data.api.login.LoginRequest +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import pbd.tubes.exe_android.ui.NetworkSensing.NetworkLiveData +import pbd.tubes.exe_android.ui.login.TokenExpirationCheckService + +class LoginActivity : AppCompatActivity() { + + private lateinit var apiService: ApiService + private lateinit var emailEditText: EditText + private lateinit var passwordEditText: EditText + private lateinit var loginButton: Button + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val networkStatusLiveData = NetworkLiveData(this) + networkStatusLiveData.observe(this, Observer { isConnected -> + if (!isConnected) { + showNoInternetPopup() + } + }) + + setContentView(R.layout.activity_login) + + val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + apiService = retrofit.create(ApiService::class.java) + + loginButton = findViewById(R.id.loginButton) + emailEditText = findViewById(R.id.emailEditText) + passwordEditText = findViewById(R.id.passwordEditText) + + loginButton.setOnClickListener { +// val email = emailEditText.text.toString() +// val password = passwordEditText.text.toString() + val email = "1@std.stei.itb.ac.id" + val password = "password_1" + val loginRequest = LoginRequest(email, password) + login(loginRequest) + + } + } + private fun login(loginRequest: LoginRequest) { + CoroutineScope(Dispatchers.IO).launch { + try { + val response = apiService.login(loginRequest) + if (response.isSuccessful) { + val token = response.body()?.token + token?.let { tkn -> + + val responseDecode = apiService.decodeToken("Bearer $tkn") + + if (responseDecode.isSuccessful) { + val expire = responseDecode.body()?.exp + expire?.let {exp -> + startActivity(Intent(this@LoginActivity, MainActivity::class.java)) + saveTokenExpiration(exp) + saveToken(tkn) + val serviceIntent = Intent(this@LoginActivity, TokenExpirationCheckService::class.java) + startService(serviceIntent) + finish() + } + } + else { + Log.d("Failed Token", "Failed Get decoded token (error response)") + } + + + } + Toast.makeText(baseContext, + "Log In Success!", + Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(baseContext, + "Log In Failed!", + Toast.LENGTH_SHORT).show() + Log.d("MyApp", "Failed login (error response)") + } + } catch (e: Exception) { + Log.d("MyApp", "$e") + } + } + } + + private fun saveToken(token: String) { + val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) + sharedPreferences.edit().putString("token", token).apply() + } + + private fun saveTokenExpiration(expirationTime: Long) { + val sharedPreferences = getSharedPreferences("user_session", MODE_PRIVATE) + sharedPreferences.edit().putLong("expiration_time", expirationTime).apply() + } + + private fun showNoInternetPopup() { + AlertDialog.Builder(this) + .setTitle("No Internet Connection") + .setMessage("Please check your internet connection and try again.") + .setPositiveButton("OK", null) + .show() + } + + companion object { + private const val BASE_URL = "https://pbd-backend-2024.vercel.app" + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt b/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..5711465e797474d7b1e97a4329632c7e09f056ac --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/login/TokenExpirationService.kt @@ -0,0 +1,49 @@ +package pbd.tubes.exe_android.ui.login +import android.app.Notification +import android.app.Service +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.util.Log +import pbd.tubes.exe_android.LoginActivity + +class TokenExpirationCheckService : Service() { + + private val sharedPreferences by lazy { + getSharedPreferences("user_session", MODE_PRIVATE) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + checkTokenExpiration() + return START_STICKY + } + + private fun checkTokenExpiration() { + val expirationTime = sharedPreferences.getLong("expiration_time", 0) + val currentTime = System.currentTimeMillis() / 1000 + val timeDifference = expirationTime - currentTime +// Log.d("Difference", "difference is $timeDifference") + + if (currentTime > expirationTime) { + redirectToLogin() + stopSelf() + return + } + + Handler(Looper.getMainLooper()).postDelayed({ + checkTokenExpiration() + }, (timeDifference) * 1000) + } + + private fun redirectToLogin() { + val loginIntent = Intent(this, LoginActivity::class.java) + loginIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + sharedPreferences.edit().clear().apply() + startActivity(loginIntent) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/notifications/NotificationsFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/notifications/NotificationsFragment.kt deleted file mode 100644 index 35e8a2a87bfe2b9f6f7fee932e14b8d9c2951971..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/notifications/NotificationsFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package pbd.tubes.exe_android.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 pbd.tubes.exe_android.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/pbd/tubes/exe_android/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/notifications/NotificationsViewModel.kt deleted file mode 100644 index b4a13729318609509a70c65c61a718c7dc6fddea..0000000000000000000000000000000000000000 --- a/app/src/main/java/pbd/tubes/exe_android/ui/notifications/NotificationsViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package pbd.tubes.exe_android.ui.notifications - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class NotificationsViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is notifications Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..1fa15afc16f4f210841ab0a2747cd8311e43bf68 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt @@ -0,0 +1,339 @@ +package pbd.tubes.exe_android.ui.scan + +import android.Manifest +import android.content.ContentValues +import android.content.Context +import android.content.Context.MODE_PRIVATE +import android.content.pm.PackageManager +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.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCapture.OutputFileResults +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.viewModels +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import pbd.tubes.exe_android.R +import pbd.tubes.exe_android.data.api.ApiService +import pbd.tubes.exe_android.databinding.FragmentScanBinding +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.io.File +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + + +class ScanFragment : Fragment() { + + private var _binding: FragmentScanBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + private val retrofit: Retrofit by lazy { + Retrofit.Builder() + .baseUrl(BASE_URL) + .client(OkHttpClient.Builder().build()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + private val apiService: ApiService by lazy { + retrofit.create(ApiService::class.java) + } + + private var imageCapture: ImageCapture? = null + private lateinit var cameraExecutor: ExecutorService + private var pickMedia: ActivityResultLauncher<PickVisualMediaRequest>? = null + private val viewModel : ScanViewModel by viewModels(factoryProducer = { ScanViewModel.Factory }) + override fun onAttach(context: Context) { + super.onAttach(context) + pickMedia = registerForActivityResult( + ActivityResultContracts.PickVisualMedia() + ) { + if (it != null){ + imagePreview(it) + Log.d("MyApp", "Picked picture, selected URI: $it") + } else { + Log.d("MyApp", "No picture picked") + } + } + } + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = FragmentScanBinding.inflate(inflater, container, false) + val root: View = binding.root + if (allPermissionsGranted()) { + startCamera() + } else { + requestPermissions() + } + + binding.imageCaptureButton.setOnClickListener { takePhoto() } + binding.pickImageButton.setOnClickListener{ imageChooser() } + cameraExecutor = Executors.newSingleThreadExecutor() + return root + } + // Function to get the actual file path from URI + fun getPathFromUri(context: Context, uri: Uri): String? { + var filePath: String? = null + when (uri.scheme) { + // For "file" scheme URIs + "file" -> { + filePath = uri.path + } + // For "content" scheme URIs (API level 19 and above) + "content" -> { + filePath = + getImagePathFromUri(context, uri) + } + } + return filePath + } + + private fun getImagePathFromUri(context: Context, uri: Uri): String? { + val projection = arrayOf(MediaStore.Images.Media.DATA) + val cursor = context.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + return it.getString(columnIndex) + } + } + return null + } + private fun uploadImage(imageUri: Uri){ + lifecycleScope.launch { + val file = File(getPathFromUri(requireContext(), imageUri)!!) + val fileBody : RequestBody = RequestBody.create(MediaType.parse("image/*"), file) + val imagePart: MultipartBody.Part = MultipartBody.Part.createFormData("file", file.getName(), fileBody) + val sharedPreferences = requireContext().getSharedPreferences("user_session", MODE_PRIVATE) + try { + val uploadResponse = apiService.uploadImage( + "Bearer ${ + sharedPreferences.getString("token", null)!!}", + imagePart) + if (uploadResponse.isSuccessful) { + Log.d("MyApp", "Succeeded receiving response, adding transaction") + } else { + Log.d("MyApp", "Receiving response failed, ${uploadResponse.message()}") + } + Log.d("MyApp", uploadResponse.body()?.items?.items.toString()) + viewModel.submitTransactions(uploadResponse.body()?.items?.items!!) + + } catch (e : Exception) { + Log.e("MyApp", "jir exception: $e") + } + } + Toast.makeText(requireContext(), "Image successfully uploaded", Toast.LENGTH_SHORT).show() + } + + private fun imagePreview(imageUri: Uri){ + binding.imagePreview.setImageURI(imageUri) + binding.scanFragment.removeView(binding.viewFinder) + binding.imageCaptureButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_done_24)) + binding.imageCaptureButton.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.pemasukan_color)) + binding.pickImageButton.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_redo_24)) + binding.pickImageButton.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.pembelian_color)) + + binding.pickImageButton.setOnClickListener { + resetState() + } + binding.imageCaptureButton.setOnClickListener { + resetState() + uploadImage(imageUri) + } + } + + private fun resetState(){ + binding.scanFragment.addView(binding.viewFinder) + + binding.pickImageButton.background = ContextCompat.getDrawable(requireContext(), R.drawable.rounded_rectangle) + binding.pickImageButton.setImageDrawable(ContextCompat.getDrawable(requireContext(),R.drawable.ic_image_24)) + binding.pickImageButton.setOnClickListener { imageChooser() } + + binding.imagePreview.setImageDrawable(null) + binding.imagePreview.visibility = View.VISIBLE + + binding.imageCaptureButton.background = ContextCompat.getDrawable(requireContext(), R.drawable.camera_button) + binding.imageCaptureButton.setImageDrawable(null) + binding.imageCaptureButton.setOnClickListener { takePhoto() } + //TODO(Delete file from URI) + } + private fun imageChooser() { + pickMedia!!.launch(PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + )) + } + + private fun takePhoto() { + // Get a stable reference of the modifiable image capture use case + val imageCapture = imageCapture ?: return + // Create time stamped name and MediaStore entry. + name = SimpleDateFormat(FILENAME_FORMAT, Locale.ENGLISH) + .format(System.currentTimeMillis()) + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, name) + put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/temp") + } + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(requireContext().contentResolver, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + contentValues) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Log.e(TAG, "Photo capture failed: ${exc.message}", exc) + } + override fun + onImageSaved(output: OutputFileResults){ + imagePreview(output.savedUri!!) + Log.d(TAG, "Photo capture success") + } + } + ) + } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + + cameraProviderFuture.addListener({ + // Used to bind the lifecycle of cameras to the lifecycle owner + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + // Preview + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(binding.viewFinder.surfaceProvider) + } + + imageCapture = ImageCapture.Builder() + .build() + + val imageAnalyzer = ImageAnalysis.Builder() + .build() +// .also { +// it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma -> +// Log.d(TAG, "Average luminosity: $luma") +// }) +// } + + // Select back camera as a default + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture, imageAnalyzer) + + } catch(exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun requestPermissions() { + activityResultLauncher.launch(REQUIRED_PERMISSIONS) + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + requireContext(), it) == PackageManager.PERMISSION_GRANTED + } + + private val activityResultLauncher = + registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions()) + { permissions -> + // Handle Permission granted/rejected + var permissionGranted = true + permissions.entries.forEach { + if (it.key in REQUIRED_PERMISSIONS && !it.value) + permissionGranted = false + } + if (!permissionGranted) { + Toast.makeText(requireContext(), + "Permission request denied", + Toast.LENGTH_SHORT).show() + } else { + startCamera() + } + } +// private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer { +// +// private fun ByteBuffer.toByteArray(): ByteArray { +// rewind() // Rewind the buffer to zero +// val data = ByteArray(remaining()) +// get(data) // Copy the buffer into a byte array +// return data // Return the byte array +// } +// +// override fun analyze(image: ImageProxy) { +// +// val buffer = image.planes[0].buffer +// val data = buffer.toByteArray() +// val pixels = data.map { it.toInt() and 0xFF } +// val luma = pixels.average() +// +// listener(luma) +// +// image.close() +// } +// } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + cameraExecutor.shutdown() + } + companion object { + private const val BASE_URL = "https://pbd-backend-2024.vercel.app" + private const val TAG = "MyApp" + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" + private var name :String? = null + private val REQUIRED_PERMISSIONS = + mutableListOf ( + Manifest.permission.CAMERA, + ).apply { +// add(Manifest.permission.WRITE_EXTERNAL_STORAGE) //DEPRECATED, only for sdk <= 28, use the MediaStore.createWriteRequest intent + }.toTypedArray() + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..95f6fe8c3a7f989005b5497f4252bc21dacce37e --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt @@ -0,0 +1,41 @@ +package pbd.tubes.exe_android.ui.scan + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import kotlinx.coroutines.launch +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.api.upload.Item +import pbd.tubes.exe_android.data.api.upload.toTransaction +import pbd.tubes.exe_android.data.database.TransactionsRepository + +class ScanViewModel(private val transactionsRepository : TransactionsRepository) : ViewModel() { + fun submitTransactions(response: List<Item>){ + viewModelScope.launch { + response.map { + transactionsRepository.insertTransaction(it.toTransaction()) + Log.d("MyApp", "data : $it.name $it.nominal $it.category $it.category $it.location $it.date") + } + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras) + : T { + val application = checkNotNull(extras[APPLICATION_KEY]) +// val savedStateHandle = extras.createSavedStateHandle() + return ScanViewModel( + (application as BondomanApp).container.transactionRepository, +// savedStateHandle + ) as T + } + } + } +} diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..8fb01230815f1357eda374e56cf29b5ea1d4e06c --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsFragment.kt @@ -0,0 +1,143 @@ +package pbd.tubes.exe_android.ui.settings + +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import pbd.tubes.exe_android.LoginActivity +import pbd.tubes.exe_android.R +import pbd.tubes.exe_android.databinding.FragmentSettingsBinding +import java.io.File +import kotlin.random.Random + +class SettingsFragment : Fragment() { + + private var _binding: FragmentSettingsBinding? = null + private lateinit var fragmentManager : FragmentManager + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private val viewModel : SettingsViewModel by viewModels(factoryProducer = {SettingsViewModel.Factory}) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSettingsBinding.inflate(inflater, container, false) + val root: View = binding.root + binding.saveButton.setOnCheckedChangeListener{ _, isChecked -> + if (isChecked){ + binding.formatDaftarTransaksi.visibility = View.VISIBLE + binding.radioFormat.visibility = View.VISIBLE + binding.confirmSaveButton.visibility = View.VISIBLE + } else{ + binding.formatDaftarTransaksi.visibility = View.GONE + binding.radioFormat.visibility = View.GONE + binding.confirmSaveButton.visibility = View.GONE + } + } + binding.confirmSaveButton.setOnClickListener{ + when (binding.radioFormat.checkedRadioButtonId){ + R.id.xls_choice -> viewModel.saveXLS() + R.id.xlsx_choice -> viewModel.saveXLSX() + } + Toast.makeText(requireContext(), "File is saved", Toast.LENGTH_SHORT).show() + } + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.fetchData() + viewModel.transactionList?.observe(viewLifecycleOwner){ + it?.let { + Log.d("MyApp", "data for saving has been loaded") + } + } + + val sendButton = view.findViewById<Button>(R.id.send_button) + sendButton.setOnClickListener { + val email = "13521025@std.stei.itb.ac.id" + val subject = "Transaction Details" + val text = "Please find attached the transaction details." + + sendTransactionsViaEmail(email, subject, text) + + } + + val randomize = view.findViewById<Button>(R.id.randomize_button) + randomize.setOnClickListener { + + val randomNumber = generateRandomNumber(1000, 9999) + val intent = Intent("pbd.tubes.exe_android.ACTION_RANDOMIZE_TRANSAKSI").apply { + putExtra("randomNumber", randomNumber) + } + requireContext().sendBroadcast(intent) + Log.d("Onpause setting", "Settinganjay") + findNavController().navigate(R.id.navigation_add_transaction) + + } + + + val logoutButton = view.findViewById<Button>(R.id.log_out_button) + + + logoutButton.setOnClickListener { + logout() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onPause() { + super.onPause() + + } + private fun logout() { + val sharedPreferences = requireContext().getSharedPreferences("user_session", MODE_PRIVATE) + sharedPreferences.edit().clear().apply() + val intent = Intent(requireContext(), LoginActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + requireActivity().finish() + } + + private fun sendTransactionsViaEmail(email: String, subject: String, text: String) { + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + val filePath = File(downloadsDir, "daftar_transaksi.xlsx") + val fileUri = FileProvider.getUriForFile( + requireContext(), + requireContext().applicationContext.packageName + ".provider", + filePath + ) + val intent = Intent(Intent.ACTION_SEND) + intent.type = "message/rfc822" + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.putExtra(Intent.EXTRA_STREAM, fileUri) + intent.setPackage("com.google.android.gm") + requireContext().startActivity(intent) + } + + private fun generateRandomNumber(min: Int, max: Int): Int { + return Random.nextInt(min, max + 1) // Generates a random number between min (inclusive) and max (inclusive) + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..94ce8f88cb436c0ef1144d916a34c8085ecf5109 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/settings/SettingsViewModel.kt @@ -0,0 +1,129 @@ +package pbd.tubes.exe_android.ui.settings + +import android.os.Environment +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import kotlinx.coroutines.launch +import org.apache.poi.hssf.usermodel.HSSFWorkbook +import org.apache.poi.ss.usermodel.CellStyle +import org.apache.poi.ss.usermodel.DataFormat +import org.apache.poi.ss.util.DateFormatConverter +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.database.TransactionsRepository +import pbd.tubes.exe_android.models.Transaction +import java.io.File +import java.io.FileOutputStream +import java.util.Locale + +class SettingsViewModel( + private val transactionsRepository: TransactionsRepository +) : ViewModel() { + + var transactionList: LiveData<List<Transaction>>? = null + + fun saveXLSX(){ + val workbook = XSSFWorkbook() + val workSheet = workbook.createSheet("Daftar Transaksi") + val excelFormatPattern = DateFormatConverter.convert(Locale.ENGLISH, "dd MMMM yyyy") + val dateCellStyle: CellStyle = workbook.createCellStyle() + val poiFormat: DataFormat = workbook.createDataFormat() + dateCellStyle.dataFormat = poiFormat.getFormat(excelFormatPattern) + + var row = workSheet.createRow(0) + row.createCell(0).setCellValue("Tanggal") + row.createCell(1).setCellValue("Kategori Transaksi") + row.createCell(2).setCellValue("Nominal Transaksi") + row.createCell(3).setCellValue("Nama Transaksi") + row.createCell(4).setCellValue("Lokasi") + + for (i in transactionList!!.value!!.indices){ + row = workSheet.createRow(i+1) + val dateCell = row.createCell(0) + dateCell.setCellValue(transactionList!!.value!![i].date) + dateCell.setCellStyle(dateCellStyle) + row.createCell(1).setCellValue(transactionList!!.value!![i].category.toString()) + row.createCell(2).setCellValue(transactionList!!.value!![i].nominal) + row.createCell(3).setCellValue(transactionList!!.value!![i].name) + row.createCell(4).setCellValue(transactionList!!.value!![i].lokasi) + } + +// val tempFile = createTempFile("daftar_transaksi_", ".xlsx") + val directory = File(Environment.getExternalStorageDirectory(), "Download") + if (!directory.exists()) { + directory.mkdirs() + } + val file = File(directory, "daftar_transaksi.xlsx") + + FileOutputStream(file).use {workbook.write(it)} + workbook.close() + Log.d("MyApp", "xlsx saved") + } + fun saveXLS(){ + val workbook = HSSFWorkbook() + val workSheet = workbook.createSheet("Daftar Transaksi") + val excelFormatPattern = DateFormatConverter.convert(Locale.ENGLISH, "dd MMMM yyyy") +// val dateCellStyle: CellStyle = workbook.createCellStyle() +// val poiFormat: DataFormat = workbook.createDataFormat() +// dateCellStyle.dataFormat = poiFormat.getFormat(excelFormatPattern) + + var row = workSheet.createRow(0) + row.createCell(0).setCellValue("Tanggal") + row.createCell(1).setCellValue("Kategori Transaksi") + row.createCell(2).setCellValue("Nominal Transaksi") + row.createCell(3).setCellValue("Nama Transaksi") + row.createCell(4).setCellValue("Lokasi") + for (i in transactionList!!.value!!.indices){ + row = workSheet.createRow(i+1) + val dateCell = row.createCell(0) + dateCell.setCellValue(transactionList!!.value!![i].date) +// dateCell.setCellStyle(dateCellStyle) + row.createCell(1).setCellValue(transactionList!!.value!![i].category.toString()) + row.createCell(2).setCellValue(transactionList!!.value!![i].nominal) + row.createCell(3).setCellValue(transactionList!!.value!![i].name) + row.createCell(4).setCellValue(transactionList!!.value!![i].lokasi) + } + +// val tempFile = createTempFile("daftar_transaksi_", ".xls") + + val directory = File(Environment.getExternalStorageDirectory(), "Download") + if (!directory.exists()) { + directory.mkdirs() + } + val file = File(directory, "daftar_transaksi.xls") + + FileOutputStream(file).use {workbook.write(it)} + workbook.close() + Log.d("MyApp", "xls saved") + } + + fun fetchData(){ + viewModelScope.launch { + transactionList = transactionsRepository.getAllTransactionsByRecentStream().asLiveData() + } + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras + ) + : T { + val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) +// val savedStateHandle = extras.createSavedStateHandle() + return SettingsViewModel( + (application as BondomanApp).container.transactionRepository, +// savedStateHandle + ) as T + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..71255c71d48a18bef549700855c82ab9d0026980 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionFragment.kt @@ -0,0 +1,134 @@ +package pbd.tubes.exe_android.ui.transactions + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.google.android.material.textfield.TextInputLayout +import kotlinx.coroutines.launch +import pbd.tubes.exe_android.R +import pbd.tubes.exe_android.databinding.FragmentAddTransactionBinding +import pbd.tubes.exe_android.models.TransactionCategory + +class AddTransactionFragment : Fragment() { + private var _binding: FragmentAddTransactionBinding? = null + private val receiver = RandomizeTransactionReceiver() + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + private val viewModel: AddTransactionViewModel by viewModels(factoryProducer = { AddTransactionViewModel.Factory }) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val intentFilter = IntentFilter("pbd.tubes.exe_android.ACTION_RANDOMIZE_TRANSAKSI") + requireContext().registerReceiver(receiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddTransactionBinding.inflate(inflater, container, false) + val root: View = binding.root +// onRandomNumberReceived(0) + + val transactionName: TextInputLayout = binding.transactionNameInputLayout + viewModel.trName.observe(viewLifecycleOwner) { + transactionName.editText?.setText(it) + } + + val transactionNominal: TextInputLayout = binding.transactionNominalInputLayout + viewModel.trNominal.observe(viewLifecycleOwner) { + transactionNominal.editText?.setText(it) + } + if (random != null){ + transactionNominal.editText?.setText(random.toString()) + } else { + transactionNominal.editText?.setText("") + } + + val transactionCategoryPemasukan: RadioButton = binding.radioPemasukan + viewModel.trCategoryMasuk.observe(viewLifecycleOwner){ + transactionCategoryPemasukan.isChecked = (it == true) + } + val transactionCategoryPembelian: RadioButton = binding.radioPembelian + viewModel.trCategoryBeli.observe(viewLifecycleOwner){ + transactionCategoryPembelian.isChecked = (it == true) + } + + val transactionLocation: TextInputLayout = binding.transactionLocationInputLayout + viewModel.trLocation.observe(viewLifecycleOwner) { + transactionLocation.editText?.setText(it) + } + + + fun chooseCategory() : TransactionCategory? { + if (transactionCategoryPemasukan.isChecked){ + return TransactionCategory.PEMASUKAN + } else if (transactionCategoryPembelian.isChecked){ + return TransactionCategory.PEMBELIAN + } + return null + } + + binding.submitButton.setOnClickListener{ + viewLifecycleOwner.lifecycleScope.launch { + try { + viewModel.submitTransaction( + transactionName.editText!!.text.toString(), + transactionNominal.editText!!.text.toString(), + chooseCategory()!!, + transactionLocation.editText!!.text.toString(), + ) + Toast.makeText(requireContext(), + "Transaction ${transactionName.editText!!.text} is added", + Toast.LENGTH_SHORT).show() + findNavController().navigate(R.id.navigation_transactions) + } catch (e : NullPointerException) { + Toast.makeText(requireContext(), + "Error! Please fill in all fields", + Toast.LENGTH_SHORT).show() + Log.e("NULL", "Input has null") + } + } + } + + return root + } + + inner class RandomizeTransactionReceiver : BroadcastReceiver(){ + override fun onReceive(context: Context, intent: Intent){ + val randomNumber = intent.getIntExtra("randomNumber", 0) + random = randomNumber +// Log.d("Ini random", randomNumber.toString()) + } + } + + companion object{ + private var random: Int? = null + } + override fun onPause() { + super.onPause() + random = null + } + + override fun onDestroyView() { + super.onDestroyView() + requireContext().unregisterReceiver(receiver) + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..2bb06a7652bcd99704ff57a29879fc3a58d24ffa --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/AddTransactionViewModel.kt @@ -0,0 +1,66 @@ +package pbd.tubes.exe_android.ui.transactions + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY +import androidx.lifecycle.viewmodel.CreationExtras +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.database.TransactionsRepository +import pbd.tubes.exe_android.models.Transaction +import pbd.tubes.exe_android.models.TransactionCategory +import java.time.LocalDate +import java.time.ZoneId + +class AddTransactionViewModel(private val transactionsRepository : TransactionsRepository) : ViewModel() { + + private val _trName = MutableLiveData<String>() + private val _trNominal = MutableLiveData<String>() + private val _trCategoryBeli = MutableLiveData<Boolean>() + private val _trCategoryMasuk = MutableLiveData<Boolean>() + private val _trLocation = MutableLiveData<String>() + + val trName: LiveData<String> = _trName + val trNominal: LiveData<String> = _trNominal + val trCategoryBeli: LiveData<Boolean> = _trCategoryBeli + val trCategoryMasuk: LiveData<Boolean> = _trCategoryMasuk + val trLocation: LiveData<String> = _trLocation + suspend fun submitTransaction( + name: String, + nominal: String, + category: TransactionCategory, + location: String + ){ + val date = LocalDate.now(ZoneId.systemDefault()) + transactionsRepository.insertTransaction( + Transaction( + id = 0, + name = name, + nominal = nominal.toDoubleOrNull() ?: 0.0, + category = category, + lokasi = location, + date = date, + ) + ) + Log.d("MyApp", "data : $name $nominal $category $category $location $date") + } + + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras) + : T { + val application = checkNotNull(extras[APPLICATION_KEY]) +// val savedStateHandle = extras.createSavedStateHandle() + return AddTransactionViewModel( + (application as BondomanApp).container.transactionRepository, +// savedStateHandle + ) as T + } + } + } +} diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..205a94fb2b0ae1421626f652598ce9139b329560 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionFragment.kt @@ -0,0 +1,30 @@ +package pbd.tubes.exe_android.ui.transactions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import pbd.tubes.exe_android.databinding.FragmentEditTransactionBinding +import pbd.tubes.exe_android.models.Transaction + +class EditTransactionFragment(transaction: Transaction) : Fragment() { + private var _binding : FragmentEditTransactionBinding? = null + private val binding get() = _binding!! + val viewModel: EditTransactionViewModel by viewModels(factoryProducer = { + EditTransactionViewModel.Factory(transaction) + }) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEditTransactionBinding.inflate(inflater, container, false) + val root: View = binding.root + + + + return root + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3a06cf028826271f68802edd3319a670e220950b --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/EditTransactionViewModel.kt @@ -0,0 +1,34 @@ +package pbd.tubes.exe_android.ui.transactions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.database.TransactionsRepository +import pbd.tubes.exe_android.models.Transaction + +class EditTransactionViewModel( + private val transactionsRepository: TransactionsRepository, + private val transaction: Transaction +) : ViewModel() { + companion object { + class EditTransactionViewModelFactory( + private val transaction: Transaction + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras + ): T { + val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) + return EditTransactionViewModel( + (application as BondomanApp).container.transactionRepository, + transaction) + as T + } + } + val Factory: (Transaction) -> ViewModelProvider.Factory = { + EditTransactionViewModelFactory(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..342426974bc1c5de496b2957b7e9928b0a4eb3df --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsFragment.kt @@ -0,0 +1,80 @@ +package pbd.tubes.exe_android.ui.transactions + +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.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import pbd.tubes.exe_android.databinding.FragmentTransactionsBinding +import pbd.tubes.exe_android.ui.transactions.component.TransactionListAdapter + +class TransactionsFragment : Fragment() { + private lateinit var recyclerView: RecyclerView + private lateinit var transactionListAdapter: TransactionListAdapter + private var _binding: FragmentTransactionsBinding? = null + private val viewModel: TransactionsViewModel by viewModels( + factoryProducer = { TransactionsViewModel.Factory } + ) + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + // TODO(filter by location by opening Google Maps) + // TODO(transaction order is only by date and still reversed on the same day) + // possible solution: order by ID instead + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentTransactionsBinding.inflate(inflater, container, false) + + val root: View = binding.root + + recyclerView = binding.transactionRecycler + transactionListAdapter = TransactionListAdapter( + onDeleteClickListener = { + Log.d("MyApp", "Delete button is clicked") + viewModel.deleteItem(it) + Toast.makeText(requireContext(), + "Transaction ${it.name} has been deleted", + Toast.LENGTH_SHORT).show() + }, + onEditClickListener = { + Log.d("MyApp", "Edit button is clicked") +// TODO("Navigation with arguments is not implemented yet") +// val bundle = bundleOf("transaction" to it) +// findNavController().navigate( +// .actionNavigationTransactionsToNavigationEditTransaction(bundle) +// ) + } + ) + recyclerView.adapter = transactionListAdapter + + recyclerView.layoutManager = LinearLayoutManager(requireContext(), + RecyclerView.VERTICAL, + false + ) + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.fetchData() + viewModel.transactionList?.observe(viewLifecycleOwner){ + it?.let { + transactionListAdapter.submitList(it) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c46d829a673d6ad06f3606878f12e223077fa77a --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/TransactionsViewModel.kt @@ -0,0 +1,48 @@ +package pbd.tubes.exe_android.ui.transactions + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.CreationExtras +import kotlinx.coroutines.launch +import pbd.tubes.exe_android.BondomanApp +import pbd.tubes.exe_android.data.database.TransactionsRepository +import pbd.tubes.exe_android.models.Transaction + +class TransactionsViewModel(private val transactionsRepository: TransactionsRepository) : ViewModel() { + var transactionList: LiveData<List<Transaction>>? = null + //TODO(handling empty list) + //TODO(fragment(or MainActivity?) margin(or padding?) is too low) + fun fetchData(){ + viewModelScope.launch { + transactionList = transactionsRepository.getAllTransactionsByRecentStream().asLiveData() + } + } + fun deleteItem(transaction: Transaction){ + viewModelScope.launch { + Log.d("MyApp", "Delete is sent to repo") + transactionsRepository.deleteTransaction(transaction) + fetchData() + } + } + companion object { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras + ) + : T { + val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) +// val savedStateHandle = extras.createSavedStateHandle() + return TransactionsViewModel( + (application as BondomanApp).container.transactionRepository, +// savedStateHandle + ) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionListAdapter.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..4009fd60a87a119b0151d558fb754302294e9d0e --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionListAdapter.kt @@ -0,0 +1,28 @@ +package pbd.tubes.exe_android.ui.transactions.component + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import pbd.tubes.exe_android.models.Transaction + +class TransactionListAdapter( + private val onDeleteClickListener: (Transaction) -> Unit, + private val onEditClickListener: (Transaction) -> Unit +) : ListAdapter<Transaction, TransactionViewHolder> (TransactionComparator()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionViewHolder { + return TransactionViewHolder.create(parent,onDeleteClickListener,onEditClickListener) + } + override fun onBindViewHolder(holder: TransactionViewHolder, position: Int) { + val current = getItem(position) + holder.bind(current) + } + class TransactionComparator : DiffUtil.ItemCallback<Transaction>() { + override fun areItemsTheSame(oldItem: Transaction, newItem: Transaction): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: Transaction, newItem: Transaction): Boolean { + return oldItem.id == newItem.id + } + } +} \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionViewHolder.kt b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..83a6b373158b265ddf7712ed65bc2f185bf60157 --- /dev/null +++ b/app/src/main/java/pbd/tubes/exe_android/ui/transactions/component/TransactionViewHolder.kt @@ -0,0 +1,56 @@ +package pbd.tubes.exe_android.ui.transactions.component + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import pbd.tubes.exe_android.R +import pbd.tubes.exe_android.models.Transaction +import pbd.tubes.exe_android.models.TransactionCategory + +class TransactionViewHolder( + itemView : View, + private val deleteListener: ((Transaction) -> Unit), + private val editListener: ((Transaction) -> Unit) +): RecyclerView.ViewHolder(itemView){ + //bindings + private val item_name: TextView = itemView.findViewById(R.id.item_name) + private val item_nominal: TextView = itemView.findViewById(R.id.item_nominal) + private val item_category: TextView = itemView.findViewById(R.id.item_category) + private val item_location: TextView = itemView.findViewById(R.id.item_location) + private val item_date: TextView = itemView.findViewById(R.id.item_date) + private var item_delete: ImageButton = itemView.findViewById(R.id.delete_card) + private var item_edit: ImageButton = itemView.findViewById(R.id.edit_card) + fun bind (transaction: Transaction) { + item_name.text = transaction.name + item_nominal.text = String.format("IDR %.2f", transaction.nominal) + item_category.text = transaction.category.toString().lowercase().replaceFirstChar { it.uppercase() } + if (transaction.category == TransactionCategory.PEMASUKAN) { + item_category.background = ContextCompat.getDrawable(itemView.context, R.drawable.pemasukan_tag) + } else if (transaction.category == TransactionCategory.PEMBELIAN) { + item_category.background = ContextCompat.getDrawable(itemView.context, R.drawable.pembelian_tag) + } + item_location.text = transaction.lokasi + item_date.text = transaction.date.toString() + item_delete.setOnClickListener{ + deleteListener.invoke(transaction) + } + item_edit.setOnClickListener{ + editListener.invoke(transaction) + } + } + + companion object { + fun create(parent: ViewGroup, + deleteListener: ((Transaction) -> Unit), + editListener: ((Transaction) -> Unit)) + : TransactionViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.transaction_list_item, parent, false) + return TransactionViewHolder(view, deleteListener, editListener) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bondoman.png b/app/src/main/res/drawable/bondoman.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5f5a2b020249fbbc169ca7a52f7010bd2514de Binary files /dev/null and b/app/src/main/res/drawable/bondoman.png differ diff --git a/app/src/main/res/drawable/camera_button.xml b/app/src/main/res/drawable/camera_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..65e1847247697741490cc45d8f4f2ca80383bccc --- /dev/null +++ b/app/src/main/res/drawable/camera_button.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:state_pressed="true"> + <shape + android:shape="oval" + android:tint="@color/gray_200"/> + </item> + <item> + <shape + android:shape="oval" + android:tint="@color/white"/> + </item> +</selector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_24.xml b/app/src/main/res/drawable/ic_add_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..9f83b8fbe793b0df45feab2a5a20f9af548bb136 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml deleted file mode 100644 index 46fc8deec32bb06fedfe594815e568e3a6bf48b9..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ -<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="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" /> -</vector> diff --git a/app/src/main/res/drawable/ic_delete_24.xml b/app/src/main/res/drawable/ic_delete_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..883bcaaccbd72b62d9cfc1dd61a0b38c873346a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_document_scanner_24.xml b/app/src/main/res/drawable/ic_document_scanner_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..c32313b67dcd717800df1b3b0e24c31b32d5dbcd --- /dev/null +++ b/app/src/main/res/drawable/ic_document_scanner_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M7,3H4v3H2V1h5V3zM22,6V1h-5v2h3v3H22zM7,21H4v-3H2v5h5V21zM20,18v3h-3v2h5v-5H20zM19,18c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V6c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2V18zM15,8H9v2h6V8zM15,11H9v2h6V11zM15,14H9v2h6V14z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_done_24.xml b/app/src/main/res/drawable/ic_done_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..adb7ae67ef8b5bfd3d948d55db5258bf39b5b0de --- /dev/null +++ b/app/src/main/res/drawable/ic_done_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..3c53db7ec89d67f61bef8a823938a980e5668736 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_history_24.xml b/app/src/main/res/drawable/ic_history_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..a4749974d0724eb9fa724bb5d111444fd54e2912 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml deleted file mode 100644 index f8bb0b55633d3e41228e9e285149af5f1d839d08..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_home_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ -<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="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" /> -</vector> diff --git a/app/src/main/res/drawable/ic_image_24.xml b/app/src/main/res/drawable/ic_image_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd890cefb4f1544083da72c67c5b5989ace76aa0 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_location_pin_24.xml b/app/src/main/res/drawable/ic_location_pin_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..dd88a43b8d529d640038f9cddf459e5e3c0cd40e --- /dev/null +++ b/app/src/main/res/drawable/ic_location_pin_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M12,2L12,2C8.13,2 5,5.13 5,9c0,1.74 0.5,3.37 1.41,4.84c0.95,1.54 2.2,2.86 3.16,4.4c0.47,0.75 0.81,1.45 1.17,2.26C11,21.05 11.21,22 12,22h0c0.79,0 1,-0.95 1.25,-1.5c0.37,-0.81 0.7,-1.51 1.17,-2.26c0.96,-1.53 2.21,-2.85 3.16,-4.4C18.5,12.37 19,10.74 19,9C19,5.13 15.87,2 12,2zM12,11.75c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.75 12,11.75z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml deleted file mode 100644 index 78b75c39b55bc3cbbc18bfe7d9165fb3da7d03ff..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ -<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,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" /> -</vector> diff --git a/app/src/main/res/drawable/ic_pie_chart_24.xml b/app/src/main/res/drawable/ic_pie_chart_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..db98ba9c6866aa4d9fc389916cfd974c6dc9d5b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_pie_chart_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M11,2v20c-5.07,-0.5 -9,-4.79 -9,-10s3.93,-9.5 9,-10zM13.03,2v8.99L22,10.99c-0.47,-4.74 -4.24,-8.52 -8.97,-8.99zM13.03,13.01L13.03,22c4.74,-0.47 8.5,-4.25 8.97,-8.99h-8.97z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_receipt_24.xml b/app/src/main/res/drawable/ic_receipt_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..181d1d03be83a35b691f5f08ea6f688e3ba84542 --- /dev/null +++ b/app/src/main/res/drawable/ic_receipt_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M18,17L6,17v-2h12v2zM18,13L6,13v-2h12v2zM18,9L6,9L6,7h12v2zM3,22l1.5,-1.5L6,22l1.5,-1.5L9,22l1.5,-1.5L12,22l1.5,-1.5L15,22l1.5,-1.5L18,22l1.5,-1.5L21,22L21,2l-1.5,1.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2 7.5,3.5 6,2 4.5,3.5 3,2v20z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_redo_24.xml b/app/src/main/res/drawable/ic_redo_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..39b0396e956a09945ec2e43475816b9ade25a6e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_redo_24.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> + + <path android:fillColor="@android:color/white" android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_settings_24.xml b/app/src/main/res/drawable/ic_settings_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..298a5a1ff28c4ce6a5591285f569dc6c827e1b15 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/> +</vector> diff --git a/app/src/main/res/drawable/pemasukan_tag.xml b/app/src/main/res/drawable/pemasukan_tag.xml new file mode 100644 index 0000000000000000000000000000000000000000..db06b5c47666f863115521a0b8cff6bb34838656 --- /dev/null +++ b/app/src/main/res/drawable/pemasukan_tag.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/pemasukan_color" /> + + <corners android:radius="10dip" /> + + <stroke + android:width="2dp" + android:color="@android:color/transparent"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/pembelian_tag.xml b/app/src/main/res/drawable/pembelian_tag.xml new file mode 100644 index 0000000000000000000000000000000000000000..8a61363eb96217c6ca3fbdbda4b5c0a180382187 --- /dev/null +++ b/app/src/main/res/drawable/pembelian_tag.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/pembelian_color" /> + + <corners android:radius="10dip" /> + + <stroke + android:width="2dp" + android:color="@android:color/transparent"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_rectangle.xml b/app/src/main/res/drawable/rounded_rectangle.xml new file mode 100644 index 0000000000000000000000000000000000000000..0c78b28c835b9eaa417801eb6872cc3bc006561e --- /dev/null +++ b/app/src/main/res/drawable/rounded_rectangle.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@android:color/white" /> + + <corners android:radius="10dip" /> + + <stroke + android:width="2dp" + android:color="@android:color/transparent"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..7c8cd7b4fc15eb3a7d61eeb1f00d0533dd7d87f3 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,48 @@ +<?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.navigationrail.NavigationRailView + android:id="@+id/nav_view_rail" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_marginStart="0dp" + android:layout_marginEnd="0dp" + android:background="?android:attr/windowBackground" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:menu="@menu/bottom_nav_menu" + /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/add_fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_add_24" + android:layout_marginBottom="16dp" + android:layout_marginEnd="16dp" + android:contentDescription="@string/transaction_operations_fab_text" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_gravity="bottom|end" + android:backgroundTint="@color/design_default_color_primary" + /> + + <fragment + android:id="@+id/nav_host_fragment_activity_main" + android:name="androidx.navigation.fragment.NavHostFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:defaultNavHost="true" + app:layout_constraintBottom_toTopOf="parent" + app:layout_constraintLeft_toRightOf="@id/nav_view_rail" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/mobile_navigation" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file 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..c1a502cd24001ad452010b8205e553d41e13c5ab --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:gravity="center" + tools:ignore="HardcodedText"> + + <TextView + android:id="@+id/loginTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Login" + android:textSize="20sp" + android:layout_centerHorizontal="true" + android:layout_marginBottom="16dp"/> + + <EditText + android:id="@+id/emailEditText" + android:layout_centerHorizontal="true" + android:minWidth="300dp" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/loginTextView" + android:layout_margin="16dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:hint="Email" + android:inputType="textEmailAddress" + android:minHeight="48dp" + /> + + <EditText + android:id="@+id/passwordEditText" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="wrap_content" + android:minWidth="300dp" + android:layout_height="wrap_content" + android:layout_below="@id/emailEditText" + android:layout_margin="16dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:hint="Password" + android:inputType="textPassword" + android:minHeight="48dp" + android:layout_centerHorizontal="true" + /> + + <Button + android:id="@+id/loginButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Login" + android:layout_below="@id/passwordEditText" + android:layout_centerHorizontal="true" + android:layout_marginTop="24dp"/> + +</RelativeLayout> \ 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..e9ae331a67257a17eec216858e353724ddb53024 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,25 +4,37 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="?attr/actionBarSize"> + > <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/nav_view" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="0dp" - android:layout_marginEnd="0dp" android:background="?android:attr/windowBackground" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_nav_menu" /> + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/add_fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_add_24" + android:layout_marginBottom="16dp" + android:layout_marginEnd="16dp" + android:contentDescription="@string/transaction_operations_fab_text" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/nav_view" + android:layout_gravity="bottom|end" + android:backgroundTint="@color/design_default_color_primary" + /> + <fragment android:id="@+id/nav_host_fragment_activity_main" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toTopOf="@id/nav_view" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/fragment_add_transaction.xml b/app/src/main/res/layout/fragment_add_transaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..f7b8a418e836dc5e76dac32923bc34b713487009 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_transaction.xml @@ -0,0 +1,127 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + 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.transactions.AddTransactionFragment" + tools:ignore="HardcodedText"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_gravity="center_horizontal" + > + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionNameInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_name_text" + android:singleLine="true" + android:textColorHint="@android:color/transparent" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionNominalInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionNameInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_nominal_text" + android:inputType="number" + android:singleLine="true" + android:textColorHint="@android:color/transparent" + android:value="" /> + </com.google.android.material.textfield.TextInputLayout> + + <TextView + android:id="@+id/transaction_category" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:text="@string/transaction_category" + app:layout_constraintBottom_toTopOf="@+id/categoryRadioGroup" + app:layout_constraintEnd_toEndOf="@+id/categoryRadioGroup" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="@+id/categoryRadioGroup" + app:layout_constraintTop_toBottomOf="@+id/transactionNominalInputLayout" /> + + <RadioGroup + android:id="@+id/categoryRadioGroup" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="300dp" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transaction_category"> + + <RadioButton + android:id="@+id/radioPembelian" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="150dp" + android:text="Pembelian" /> + + <RadioButton + android:id="@+id/radioPemasukan" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="150dp" + android:text="Pemasukan" /> + </RadioGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionLocationInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/categoryRadioGroup" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_location" + android:singleLine="true" + android:textColorHint="@android:color/transparent" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/submit_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:text="Submit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionLocationInputLayout" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chart.xml b/app/src/main/res/layout/fragment_chart.xml new file mode 100644 index 0000000000000000000000000000000000000000..9ddcc4a3d325147bb1b17f5962d949971734130d --- /dev/null +++ b/app/src/main/res/layout/fragment_chart.xml @@ -0,0 +1,33 @@ +<?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.chart.ChartFragment" + > + +<!-- <TextView--> +<!-- android:layout_width="wrap_content"--> +<!-- android:layout_height="wrap_content"--> +<!-- app:layout_constraintTop_toTopOf="parent"--> +<!-- app:layout_constraintStart_toStartOf="parent"--> +<!-- app:layout_constraintEnd_toEndOf="parent"--> +<!-- android:layout_marginTop="32dp"--> +<!-- app:layout_constraintHorizontal_bias="0.15"--> +<!-- />--> + + <com.github.mikephil.charting.charts.PieChart + android:id="@+id/pieChart" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="300dp" + android:minHeight="300dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.3" /> +<!-- app:layout_constraintBottom_toBottomOf="parent"--> + +</androidx.constraintlayout.widget.ConstraintLayout> \ 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_transaction.xml b/app/src/main/res/layout/fragment_edit_transaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..84371848763e2f0357372c8440303bf72ada6637 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_transaction.xml @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView 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:ignore="HardcodedText" + tools:context=".ui.transactions.EditTransactionFragment" + > + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionEditNameInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_name_text" + android:singleLine="true" + android:textColorHint="@android:color/transparent" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionEditNominalInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionEditNameInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_nominal_text" + android:inputType="number" + android:singleLine="true" + android:textColorHint="@android:color/transparent" + android:value="" /> + </com.google.android.material.textfield.TextInputLayout> + + <TextView + android:id="@+id/transactionEdit_category" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:text="@string/transaction_category" + app:layout_constraintBottom_toTopOf="@+id/categoryRadioGroup" + app:layout_constraintEnd_toEndOf="@+id/categoryRadioGroup" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="@+id/categoryRadioGroup" + app:layout_constraintTop_toBottomOf="@+id/transactionEditDateInputLayout" /> + + <RadioGroup + android:id="@+id/categoryRadioGroup" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="300dp" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionEdit_category" + android:clickable="false" + > + + <RadioButton + android:id="@+id/radioPembelian" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="150dp" + android:text="Pembelian" + android:enabled="false" + android:clickable="false" + /> + + <RadioButton + android:id="@+id/radioPemasukan" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="150dp" + android:text="Pemasukan" + android:enabled="false" + android:clickable="false" + /> + </RadioGroup> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionEditLocationInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionEditNominalInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/transaction_location" + android:singleLine="true" + android:textColorHint="@android:color/transparent" + /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/transactionEditDateInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/transactionEditLocationInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="Tanggal" + android:singleLine="true" + android:textColorHint="@android:color/transparent" + android:enabled="false" + android:clickable="false" + /> + + </com.google.android.material.textfield.TextInputLayout> + + + <Button + android:id="@+id/submit_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Submit" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/categoryRadioGroup" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> \ 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.xml b/app/src/main/res/layout/fragment_scan.xml new file mode 100644 index 0000000000000000000000000000000000000000..7fd955ab425a71c042772c8f244d77f301d7dc5d --- /dev/null +++ b/app/src/main/res/layout/fragment_scan.xml @@ -0,0 +1,62 @@ +<?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/scan_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.scan.ScanFragment"> + + <androidx.camera.view.PreviewView + android:id="@+id/viewFinder" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + </androidx.camera.view.PreviewView> + + <ImageButton + android:id="@+id/image_capture_button" + android:layout_width="110dp" + android:layout_height="110dp" + android:layout_marginBottom="50dp" + android:background="@drawable/camera_button" + android:contentDescription="@string/take_picture" + android:elevation="1dp" + android:text="@string/take_photo" + android:scaleType="fitXY" + android:padding="24dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <ImageButton + android:id="@+id/pick_image_button" + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_marginEnd="32dp" + android:background="@drawable/rounded_rectangle" + android:contentDescription="@string/pick_picture" + android:elevation="1dp" + android:minWidth="50dp" + android:minHeight="50dp" + android:src="@drawable/ic_image_24" + app:layout_constraintBottom_toBottomOf="@+id/image_capture_button" + app:layout_constraintEnd_toStartOf="@+id/image_capture_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/image_capture_button" /> + + <ImageView + android:id="@+id/image_preview" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="50dp" + android:contentDescription="@string/photo_preview" + android:elevation="2dp" + android:visibility="visible" + app:layout_constraintBottom_toTopOf="@+id/image_capture_button" + 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_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..86f2a2300f62f6e866cfcb80082a1f66526f7028 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,110 @@ +<?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" + tools:ignore="HardcodedText" + android:id="@+id/fragment_settings" + > + + <ToggleButton + android:id="@+id/save_button" + android:textColor="@android:color/black" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:textOn="Simpan daftar transaksi" + android:textOff="Simpan daftar transaksi" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + <TextView + android:id="@+id/format_daftar_transaksi" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Format daftar transaksi:" + app:layout_constraintTop_toBottomOf="@id/save_button" + app:layout_constraintLeft_toLeftOf="@id/save_button" + app:layout_constraintRight_toRightOf="@id/save_button" + app:layout_constraintHorizontal_bias="0.15" + android:visibility="gone" + /> + <RadioGroup + android:id="@+id/radio_format" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="start" + app:layout_constraintLeft_toLeftOf="@id/format_daftar_transaksi" + app:layout_constraintRight_toRightOf="@id/format_daftar_transaksi" + app:layout_constraintTop_toBottomOf="@id/format_daftar_transaksi" + android:orientation="vertical" + app:layout_constraintHorizontal_bias="0.15" + android:visibility="gone" + > + <RadioButton + android:id="@+id/xls_choice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text=".xls"/> + <RadioButton + android:id="@+id/xlsx_choice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text=".xlsx"/> + </RadioGroup> + <Button + android:id="@+id/confirm_save_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="300dp" + style="@style/Widget.Material3.Button" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:text="SAVE" + app:layout_constraintLeft_toLeftOf="@id/radio_format" + app:layout_constraintTop_toBottomOf="@id/radio_format" + android:visibility="gone" + /> + + <Button + android:id="@+id/send_button" + style="@style/Widget.Material3.Button" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:backgroundTint="@color/gray_200" + android:textColor="@android:color/black" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Kirim daftar transaksi" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/save_button" + app:layout_constraintTop_toBottomOf="@+id/confirm_save_button" /> + + <Button + android:id="@+id/randomize_button" + style="@style/Widget.Material3.Button" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:backgroundTint="@color/gray_200" + android:textColor="@android:color/black" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Randomize transaksi" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/save_button" + app:layout_constraintTop_toBottomOf="@+id/send_button" /> + + <Button + android:id="@+id/log_out_button" + style="@style/Widget.Material3.Button" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:textColor="@color/design_default_color_error" + android:backgroundTint="@color/gray_200" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Keluar" + app:layout_constraintEnd_toEndOf="@+id/send_button" + app:layout_constraintStart_toStartOf="@+id/send_button" + app:layout_constraintTop_toBottomOf="@+id/randomize_button" /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_transactions.xml similarity index 55% rename from app/src/main/res/layout/fragment_home.xml rename to app/src/main/res/layout/fragment_transactions.xml index f3d9b08ffe6101e25c77c5fae7e28bb5dfa11fbd..de5fa8741f69cb7b0d845b6b211b47cc231bc7a5 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_transactions.xml @@ -4,19 +4,15 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.home.HomeFragment"> + tools:context=".ui.transactions.TransactionsFragment"> - <TextView - android:id="@+id/text_home" - android:layout_width="match_parent" + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/transaction_recycler" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + + /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/transaction_list_item.xml b/app/src/main/res/layout/transaction_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..4bf99a3422e51192593ddc386415cc59778e2463 --- /dev/null +++ b/app/src/main/res/layout/transaction_list_item.xml @@ -0,0 +1,122 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + app:cardCornerRadius="16dp" + android:minWidth="300dp" + app:cardBackgroundColor="@color/gray_200" + android:minHeight="100dp" + android:layout_marginBottom="32dp" + tools:ignore="HardcodedText" + > + + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center|start" + android:minHeight="100dp" + android:minWidth="300dp" + > + + <FrameLayout + android:id="@+id/item_detail" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:minHeight="100dp" + android:minWidth="300dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/item_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center|start" + android:layout_margin="8dp" + android:text="@string/transaction_name_text" + android:textColor="@color/black" + /> + + <TextView + android:id="@+id/item_nominal" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|start" + android:layout_margin="8dp" + android:textColor="@color/black" + android:text="@string/transaction_nominal_text" /> + + <TextView + android:id="@+id/item_category" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|top" + android:layout_margin="8dp" + android:textColor="@color/black" + android:text="@string/transaction_category" + android:background="@drawable/pembelian_tag" + android:paddingHorizontal="8dp" + /> + + <TextView + android:id="@+id/item_location" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="8dp" + android:textColor="@color/black" + android:text="@string/transaction_location" + app:drawableEndCompat="@drawable/ic_location_pin_24" /> + + <TextView + android:id="@+id/item_date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|start" + android:layout_margin="8dp" + android:textColor="@color/black" + android:text="DD-MM-YYYY" + android:contentDescription="Tanggal transaksi" + /> + </FrameLayout> + + <LinearLayout + android:layout_width="300dp" + android:layout_height="match_parent" + android:minWidth="300dp" + android:minHeight="48dp" + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/item_detail" + > + <!--TODO(Edit Transaction has bug)--> + <ImageButton + android:id="@+id/edit_card" + android:layout_width="0dp" + android:layout_height="match_parent" + android:background="@color/cardview_light_background" + android:src="@drawable/ic_edit_24" + android:layout_weight="1" + android:contentDescription="Edit Button" + android:visibility="gone" + /> + + <ImageButton + android:id="@+id/delete_card" + android:layout_width="0dp" + android:layout_height="match_parent" + android:background="@color/design_default_color_error" + android:src="@drawable/ic_delete_24" + android:layout_weight="1" + android:contentDescription="Delete" + /> + </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.cardview.widget.CardView> \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index fb6d040b9a026ddaa05df0e530ab6f95e6c1f999..0301c9a023044586da5a44233bc711bb37a896c0 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -1,19 +1,26 @@ <?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android"> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/bottom_nav_menu" + > <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_history_24" + 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_scan" + android:icon="@drawable/ic_document_scanner_24" + android:title="@string/title_scan" /> <item - android:id="@+id/navigation_notifications" - android:icon="@drawable/ic_notifications_black_24dp" - android:title="@string/title_notifications" /> + android:id="@+id/navigation_chart" + android:icon="@drawable/ic_pie_chart_24" + android:title="@string/title_chart" /> + + <item + android:id="@+id/navigation_settings" + android:icon="@drawable/ic_settings_24" + android:title="@string/title_settings" /> </menu> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000000000000000000000000000000000..036d09bc5fd523323794379703c4a111d1e28a04 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <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-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000000000000000000000000000000000..036d09bc5fd523323794379703c4a111d1e28a04 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <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.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..0000000000000000000000000000000000000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?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" /> -</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 deleted file mode 100644 index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..0000000000000000000000000000000000000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?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" /> -</adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78ecd372343283f4157dcfd918ec5165bb3..f22c3481d4d691a0c26c21b8b585e698a9fe52ac 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..56fb8839d687f93864e949a8c37a2278bf4d66cd Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp 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 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..5c72ddf2acabc8a36ff32fd0b3b8e67bb1b93362 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..401b99d79487e7ad9d9450798111939d77ee760f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..e06d47490c67ee4085e9db59d73f008710f45fe9 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp 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 index 62b611da081676d42f6c3f78a2c91e7bcedddedb..3dbfa9c1d93d242d30a0edf8fc7063dfd24e44d8 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a3070fe34c611c42c0d3ad3013a0dce358be0..53c0d9f00162879c829849d7aef81f38f7b3b57f 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..cf188de4455de8411b6127cc763b1b35502941a9 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp 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 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..05f54dcf5511219ca711466bff7c9d4f63fbe4b5 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77f9f036a47549d47db79c16788749dca10..adbcbe750040240265c1925dd2242a29326ab5c2 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..da9290fd2813cb347e95ea8a6df273d452509575 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp 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 index 9287f5083623b375139afb391af71cc533a7dd37..de6a44735807a982738b81b35117858f4cb09665 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d6427e6fa1074b79ccd52ef67ac15c5637e85..3a1c2ae225d592f5fcf19cd48b2e25a7a5af319f 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..13e269998bf3797fb0d89bb0532018c95b03568c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp 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 index 9126ae37cbc3587421d6889eadd1d91fbf1994d4..e01325c62a8e5dad4a99087556dae2c679e49cf0 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index d46c7d0305e3ace935b47639d1020ff6ec3bd0c9..4e5707724034876f13c52831face882febd239a6 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -3,23 +3,55 @@ 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" + app:defaultNavHost="true" + app:navGraph="@navigation/mobile_navigation" + > <fragment - android:id="@+id/navigation_home" - android:name="pbd.tubes.exe_android.ui.home.HomeFragment" - android:label="@string/title_home" - tools:layout="@layout/fragment_home" /> + android:id="@+id/navigation_transactions" + android:name="pbd.tubes.exe_android.ui.transactions.TransactionsFragment" + android:label="@string/title_transactions" + tools:layout="@layout/fragment_transactions" + /> +<!-- >--> +<!-- <action--> +<!-- android:id="@+id/action_navigation_transactions_to_navigation_edit_transaction"--> +<!-- app:destination="@id/navigation_edit_transaction">--> +<!-- <argument--> +<!-- android:name="transaction"--> +<!-- app:argType="pbd.tubes.exe_android.models.Transaction" />--> +<!-- </action>--> +<!-- </fragment>--> <fragment - android:id="@+id/navigation_dashboard" - android:name="pbd.tubes.exe_android.ui.dashboard.DashboardFragment" - android:label="@string/title_dashboard" - tools:layout="@layout/fragment_dashboard" /> + android:id="@+id/navigation_scan" + android:name="pbd.tubes.exe_android.ui.scan.ScanFragment" + android:label="@string/title_scan" + tools:layout="@layout/fragment_scan" /> <fragment - android:id="@+id/navigation_notifications" - android:name="pbd.tubes.exe_android.ui.notifications.NotificationsFragment" - android:label="@string/title_notifications" - tools:layout="@layout/fragment_notifications" /> + android:id="@+id/navigation_chart" + android:name="pbd.tubes.exe_android.ui.chart.ChartFragment" + android:label="@string/title_chart" + tools:layout="@layout/fragment_chart" /> + + <fragment + android:id="@+id/navigation_settings" + android:name="pbd.tubes.exe_android.ui.settings.SettingsFragment" + android:label="@string/title_settings" + tools:layout="@layout/fragment_settings" /> + + <fragment + android:id="@+id/navigation_add_transaction" + android:name="pbd.tubes.exe_android.ui.transactions.AddTransactionFragment" + android:label="Add Transaction" + tools:layout="@layout/fragment_add_transaction"/> + + <fragment + android:id="@+id/navigation_edit_transaction" + android:name="pbd.tubes.exe_android.ui.transactions.EditTransactionFragment" + android:label="Edit Transaction" + tools:layout="@layout/fragment_edit_transaction"/> + </navigation> \ 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..995bd8411199754be09f57ce924b31ac4ab86580 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,12 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="light_blue_400">#FF29B6F6</color> + <color name="light_blue_600">#FF039BE5</color> + <color name="gray_200">#D7D7D7</color> + <color name="gray_400">#D7D7D7</color> + <color name="gray_600">#FF757575</color> + + <color name="pemasukan_color">#8EF2A4</color> + <color name="pembelian_color">#ED561C</color> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..c5d5899fdf0a1b144bf341b29e0c66ba50bbcedd --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="ic_launcher_background">#FFFFFF</color> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d27be3d5f42ed2c3cf59202e24a55e73bfaf8b71..2142b2877ac9f18fc59aa115acf9bcce18c8b566 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,19 @@ <resources> - <string name="app_name">exe_android</string> + <string name="app_name">BondoMan</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_scan">Scan</string> + <string name="title_chart">Graf</string> + <string name="title_settings">Pengaturan</string> + <string name="transaction_operations_fab_text">Add Transaction</string> + <string name="transaction_name_text">Nama Transaksi</string> + <string name="transaction_nominal_text">Nominal</string> + <string name="transaction_category">Kategori</string> + <string name="transaction_location">Lokasi</string> + <string name="take_photo">ambil foto</string> + <string name="take_picture">Take Picture</string> + <string name="pick_picture">Pick picture</string> + <string name="photo_preview">Photo Preview</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..791000e4925e4f5d623dbdb27894db7d2adc2057 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <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..161d31be377ef9dd4d613966992c0d454119c37c 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("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.android.application") version "8.3.1" apply false + id("org.jetbrains.kotlin.android") version "1.9.23" apply false + id("com.google.devtools.ksp") version "1.9.23-1.0.19" apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3c5031eb7d63f785752b1914cc8692a453d1cc63..c22da5736c4a0ff537d51c41ac7ab53331239a81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fe507cd1912cec38b4353bfa295972bd604e5bc5..82b76d38a7b3a9b11350d46d6abb00edebc58a61 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Mar 13 19:06:54 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/settings.gradle.kts b/settings.gradle.kts index e214361957eb3a305e9e620ed7ed5fc278c804c4..4670cb7836ef8495b9d3f25388f04fd0c67ae697 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven ("https://jitpack.io") } } dependencyResolutionManagement { @@ -10,6 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven ("https://jitpack.io") } }