diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d994a6599dd8b64a341af14c598069a10022..fe63bb677dc7c018519fa0fb0fecb445e5256c67 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="KotlinJpsPluginSettings"> - <option name="version" value="1.9.0" /> + <option name="version" value="1.9.23" /> </component> </project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cbd33a2f389d524bc4bfef9c52e1f7ab490..8978d23db569daa721cb26dde7923f4c673d1fc9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 036f3e83ee280797b2d2fa82b2ce2f1c2ec52912..adfc4c04e01ff796720cc45bd7bd3f6739861ddd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.androidApplication) alias(libs.plugins.jetbrainsKotlinAndroid) + alias(libs.plugins.jetbrainsKotlinKapt) } android { @@ -33,6 +34,12 @@ android { buildFeatures { viewBinding = true } + + packaging { + resources { + excludes += "META-INF/*" + } + } } dependencies { @@ -45,11 +52,19 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.annotation) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + // Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + implementation(libs.room.common) + annotationProcessor(libs.room.compiler) + kapt(libs.room.compiler) + // Retrofit implementation(libs.retrofit2.retrofit) implementation(libs.retrofit2.converter.gson) @@ -58,4 +73,7 @@ dependencies { // Work implementation(libs.androidx.work.runtime) + + // Excel + implementation(libs.excelkt) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a76744ed0fee6ca61d9a398d985824b3afa9ed2c..a0edab5f4cdbd3e858c3dec4fd20441d0c954598 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,22 +12,34 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Bondoman" - tools:targetApi="31"> + android:theme="@style/Theme.Bondoman"> <activity android:name=".ui.login.LoginActivity" android:exported="false" - android:label="@string/title_activity_login" /> + android:label="@string/title_activity_login" + android:noHistory="true" /> <activity android:name=".MainActivity" - android:exported="true" - android:label="@string/app_name"> + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + <activity + android:name=".AddTransactionActivity" + android:exported="false" /> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/AddTransactionActivity.kt b/app/src/main/java/com/onionsquad/bondoman/AddTransactionActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..e49b61b1892a12f7b0a11a5ec1e4386ab37c519f --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/AddTransactionActivity.kt @@ -0,0 +1,57 @@ +package com.onionsquad.bondoman + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.onionsquad.bondoman.databinding.ActivityAddTransactionBinding +import com.onionsquad.bondoman.repository.TransactionRepository +import com.onionsquad.bondoman.room.TransactionCategory +import com.onionsquad.bondoman.room.TransactionDatabase +import com.onionsquad.bondoman.room.TransactionEntity +import com.onionsquad.bondoman.ui.transaction.TransactionViewModel +import com.onionsquad.bondoman.ui.transaction.TransactionViewModelFactory +import java.util.Date + +class AddTransactionActivity : AppCompatActivity() { + private lateinit var binding: ActivityAddTransactionBinding + private val database by lazy { TransactionDatabase.getInstance(this) } + private val repository by lazy { TransactionRepository(database.transactionDao()) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAddTransactionBinding.inflate(layoutInflater) + setContentView(binding.root) + + val factory = TransactionViewModelFactory(repository) + val viewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] + + binding.saveButton.setOnClickListener { + val title = binding.titleEditText.text.toString() + val amount = binding.amountEditText.text.toString().toDoubleOrNull() ?: 0.0 + val selectedCategoryId = binding.categoryRadioGroup.checkedRadioButtonId + val category = when (selectedCategoryId) { + R.id.incomeRadioButton -> TransactionCategory.INCOME + R.id.outcomeRadioButton -> TransactionCategory.OUTCOME + else -> TransactionCategory.OUTCOME + } + val location = binding.locationEditText.text.toString() + + val currentDate = Date() + + val transaction = TransactionEntity( + title = title, + amount = amount, + category = category, + date = currentDate, + location = location + ) + + viewModel.insertTransaction(transaction) + + Intent(this@AddTransactionActivity, MainActivity::class.java).also { + startActivity(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt b/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt index 5e8bc1e29afe9cefe2d3a0afa6402a29c6d29fab..c0498169e12886585ae46342d9124c1244b72c7f 100644 --- a/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt +++ b/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt @@ -23,9 +23,7 @@ class MainActivity : AppCompatActivity() { AutoLogoutWorker.start(this) val sessionManager = SessionManager(this) - if (sessionManager.fetchAuthToken() == null) { - sendToLoginActivity() - } + sessionManager.ensureAuthenticated() binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -40,11 +38,4 @@ class MainActivity : AppCompatActivity() { setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) } - - private fun sendToLoginActivity() { - val intent = Intent(this@MainActivity, LoginActivity::class.java) - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK) - startActivity(intent) - finish() - } } \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/auth/SessionManager.kt b/app/src/main/java/com/onionsquad/bondoman/auth/SessionManager.kt index 91fc448a741643829bf2833dc42eae409ba2ff56..1089d87fad8930e1e229c7f55109bbc33d601161 100644 --- a/app/src/main/java/com/onionsquad/bondoman/auth/SessionManager.kt +++ b/app/src/main/java/com/onionsquad/bondoman/auth/SessionManager.kt @@ -1,11 +1,13 @@ package com.onionsquad.bondoman.auth import android.content.Context +import android.content.Intent import android.util.Log import com.google.gson.Gson import com.onionsquad.bondoman.R +import com.onionsquad.bondoman.ui.login.LoginActivity -class SessionManager(context: Context) { +class SessionManager(private val context: Context) { private val sharedPreferences = context.getSharedPreferences( context.getString(R.string.preference_file_key), Context.MODE_PRIVATE @@ -28,4 +30,12 @@ class SessionManager(context: Context) { fun deleteAuthToken() { sharedPreferences.edit().remove(USER_TOKEN).apply() } + + fun ensureAuthenticated() { + if (fetchAuthToken() == null) { + val intent = Intent(context, LoginActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + context.startActivity(intent) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d31c4ab7d591d2cb2a82a8c5af4fcd690fbeee8 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt @@ -0,0 +1,20 @@ +package com.onionsquad.bondoman.repository + +import com.onionsquad.bondoman.room.* +import androidx.lifecycle.LiveData + +class TransactionRepository(private val transactionDao: TransactionDao) { + val listTransactions: LiveData<List<TransactionEntity>> = transactionDao.getAllTransactions() + + suspend fun insertTransaction(transaction: TransactionEntity) { + transactionDao.insertTransaction(transaction) + } + + suspend fun updateTransaction(transaction: TransactionEntity) { + transactionDao.updateTransaction(transaction) + } + + suspend fun deleteTransaction(transaction: TransactionEntity) { + transactionDao.deleteTransaction(transaction) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt new file mode 100644 index 0000000000000000000000000000000000000000..f37764b16572f2d510b4d0c2a12c54abae909df7 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt @@ -0,0 +1,7 @@ +package com.onionsquad.bondoman.room + +enum class TransactionCategory { + INCOME, + OUTCOME, + UNKNOWN +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..35e64cc4e012610fb9739a15fc9e6a5132680f45 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt @@ -0,0 +1,19 @@ +package com.onionsquad.bondoman.room + +import androidx.room.* +import androidx.lifecycle.LiveData + +@Dao +interface TransactionDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTransaction(transaction: TransactionEntity) + + @Query("SELECT * FROM transactions") + fun getAllTransactions(): LiveData<List<TransactionEntity>> + + @Update + suspend fun updateTransaction(transaction: TransactionEntity) + + @Delete + suspend fun deleteTransaction(transaction: TransactionEntity) +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionDatabase.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc1fa967766e53114c2fdc3893feff5bc23a4546 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDatabase.kt @@ -0,0 +1,29 @@ +package com.onionsquad.bondoman.room + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import android.content.Context + + +@Database(entities = [TransactionEntity::class], version = 1) +abstract class TransactionDatabase : RoomDatabase() { + abstract fun transactionDao(): TransactionDao + + companion object { + @Volatile + private var instance: TransactionDatabase? = null + fun getInstance(context: Context): TransactionDatabase { + synchronized(this) { + if (instance == null) { + instance = Room.databaseBuilder( + context.applicationContext, + TransactionDatabase::class.java, + "transaction_database" + ).build() + } + return instance!! + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionEntity.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..64898cbf7c8ebf97163227d9ddd15a23450d0bc8 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionEntity.kt @@ -0,0 +1,30 @@ +package com.onionsquad.bondoman.room + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.ColumnInfo +import androidx.room.TypeConverters +import com.onionsquad.bondoman.util.Converters +import java.util.Date + +@Entity(tableName = "transactions") +@TypeConverters(Converters::class) +data class TransactionEntity( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + + @ColumnInfo(name = "title") + val title: String, + + @ColumnInfo(name = "amount") + val amount: Double, + + @ColumnInfo(name = "category") + val category: TransactionCategory, + + @ColumnInfo(name = "date", defaultValue = "CURRENT_TIMESTAMP") + val date: Date, + + @ColumnInfo(name = "location") + val location: String +) diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt b/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt index 37c78c8c5c0b91cfcb6ee44ff9bbde80ab9e6751..25a006159c73320ffb24bc1e40df328bb90ce677 100644 --- a/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt @@ -1,23 +1,72 @@ package com.onionsquad.bondoman.ui.settings import android.app.AlertDialog +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.onionsquad.bondoman.R import com.onionsquad.bondoman.auth.AutoLogoutWorker import com.onionsquad.bondoman.auth.SessionManager import com.onionsquad.bondoman.databinding.FragmentSettingsBinding +import com.onionsquad.bondoman.repository.TransactionRepository +import com.onionsquad.bondoman.room.TransactionCategory +import com.onionsquad.bondoman.room.TransactionDatabase +import com.onionsquad.bondoman.room.TransactionEntity +import io.github.evanrupert.excelkt.workbook +import org.apache.poi.ss.usermodel.FillPatternType +import org.apache.poi.ss.usermodel.IndexedColors +import java.io.File +import java.io.OutputStream +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import kotlin.io.path.createTempFile class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null private val binding get() = _binding!! + private val database by lazy { TransactionDatabase.getInstance(requireContext()) } + private val repository by lazy { TransactionRepository(database.transactionDao()) } + + + private val saveXls = + registerForActivityResult(CreateDocument("application/vnd.ms-excel")) { uri -> + if (uri != null) { + repository.listTransactions.observe(viewLifecycleOwner) { list -> + Log.d(this::class.java.simpleName, "Saving transactions to $uri") + requireContext().contentResolver.openOutputStream(uri)?.use { fos -> + createExcelFile(list, fos) + } + } + } + } + + private val saveXlsx = + registerForActivityResult( + CreateDocument("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + ) { uri -> + if (uri != null) { + repository.listTransactions.observe(viewLifecycleOwner) { list -> + Log.d(this::class.java.simpleName, "Saving transactions to $uri") + requireContext().contentResolver.openOutputStream(uri)?.use { fos -> + createExcelFile(list, fos) + } + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -25,8 +74,10 @@ class SettingsFragment : Fragment() { ): View { _binding = FragmentSettingsBinding.inflate(inflater, container, false) + val sessionManager = SessionManager(requireContext()) + binding.apply { - logoutButton.setOnClickListener { + buttonLogout.setOnClickListener { val alertBuilder = AlertDialog.Builder(requireContext()) alertBuilder.setTitle(R.string.title_alert_logout) alertBuilder.setMessage(R.string.message_alert_logout) @@ -39,6 +90,59 @@ class SettingsFragment : Fragment() { } alertBuilder.show() } + + ArrayAdapter.createFromResource( + requireContext(), + R.array.excel_types, + android.R.layout.simple_spinner_dropdown_item + ).also { arrayAdapter -> + arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinnerExcelType.adapter = arrayAdapter + } + + buttonSave.setOnClickListener { + when (spinnerExcelType.selectedItem.toString()) { + "XLS" -> saveXls.launch("transactions.xls") + "XLSX" -> saveXlsx.launch("transactions.xlsx") + } + Toast.makeText(requireContext(), R.string.transactions_saved, Toast.LENGTH_SHORT) + .show() + } + + buttonSendEmail.setOnClickListener { + sessionManager.ensureAuthenticated() + val token = sessionManager.fetchAuthToken()!! + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/html" + putExtra(Intent.EXTRA_EMAIL, arrayOf("${token.nim}@std.stei.itb.ac.id")) + putExtra(Intent.EXTRA_SUBJECT, "Daftar Transaksi Aplikasi Bondoman") + putExtra( + Intent.EXTRA_TEXT, + "Halo ${token.nim}, berikut daftar transaksi yang telah kamu catat dalam aplikasi Bondoman" + ) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val tempFile = File(requireContext().cacheDir, "transactions.xlsx") + if (tempFile.exists()) { + tempFile.delete() + } else { + tempFile.parentFile?.mkdirs() + } + val fileUri = FileProvider.getUriForFile( + requireContext(), + "${requireContext().packageName}.fileprovider", + tempFile + ) + repository.listTransactions.observe(viewLifecycleOwner) { list -> + requireContext().contentResolver.openOutputStream(fileUri)?.use { fos -> + createExcelFile(list, fos) + } + } + putExtra(Intent.EXTRA_STREAM, fileUri) + } + startActivity(Intent.createChooser(intent, "Send email")) + Toast.makeText(requireContext(), R.string.transactions_sent, Toast.LENGTH_SHORT) + .show() + } } return binding.root @@ -56,4 +160,53 @@ class SettingsFragment : Fragment() { findNavController().popBackStack(R.id.navigation_transaction, true) requireActivity().recreate() } + + private fun createExcelFile(data: List<TransactionEntity>, outputStream: OutputStream) { + workbook { + val headers = arrayOf( + "Tanggal", + "Kategori Transaksi", + "Nominal Transaksi", + "Nama Transaksi", + "Lokasi" + ) + val sheet = sheet { + val headingStyle = createCellStyle { + val font = createFont { bold = true } + setFont(font) + fillPattern = FillPatternType.SOLID_FOREGROUND + fillForegroundColor = IndexedColors.AQUA.index + } + row(headingStyle) { + headers.forEach { cell(it) } + } + for (transaction in data) { + row { + val date = transaction.date + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime() + .format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")) + cell(date) + cell(transaction.category.text()) + cell(transaction.amount) + cell(transaction.title) + cell(transaction.location) + } + } + }.xssfSheet + for (i in 0..headers.size) { + sheet.setColumnWidth(i, 30 * 256) + } + }.xssfWorkbook.write(outputStream) + } + + private fun TransactionCategory.text(): String { + return when (this) { + TransactionCategory.INCOME -> "Pemasukan" + TransactionCategory.OUTCOME -> "Pengeluaran" + TransactionCategory.UNKNOWN -> "" + } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionFragment.kt b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionFragment.kt index d43791d0f9b611492d3e60d466a0a93a6fe3c877..4ff1ccc32f50abe95a0e0e74e85e2595bf8997e6 100644 --- a/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionFragment.kt +++ b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionFragment.kt @@ -1,12 +1,16 @@ package com.onionsquad.bondoman.ui.transaction +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import com.onionsquad.bondoman.AddTransactionActivity import com.onionsquad.bondoman.databinding.FragmentTransactionBinding +import com.onionsquad.bondoman.repository.TransactionRepository +import com.onionsquad.bondoman.room.TransactionDatabase class TransactionFragment : Fragment() { @@ -16,16 +20,24 @@ class TransactionFragment : Fragment() { // onDestroyView. private val binding get() = _binding!! + private val database by lazy { TransactionDatabase.getInstance(requireContext().applicationContext) } + private val repository by lazy { TransactionRepository(database.transactionDao()) } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val transactionViewModel = - ViewModelProvider(this).get(TransactionViewModel::class.java) + val factory = TransactionViewModelFactory(repository) + val transactionViewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] _binding = FragmentTransactionBinding.inflate(inflater, container, false) + binding.button.setOnClickListener { + val intent = Intent(activity, AddTransactionActivity::class.java) + startActivity(intent) + } + return binding.root } diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModel.kt b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModel.kt index bb883eaad8241e46f1cb31cf707403419df2b22b..cf96283f3289be47bfd3205f7e6faa0ac4467748 100644 --- a/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModel.kt +++ b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModel.kt @@ -1,13 +1,24 @@ package com.onionsquad.bondoman.ui.transaction import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.onionsquad.bondoman.repository.TransactionRepository +import com.onionsquad.bondoman.room.TransactionEntity +import kotlinx.coroutines.launch -class TransactionViewModel : ViewModel() { +class TransactionViewModel(private val repository : TransactionRepository) : ViewModel() { + var listTransactions: LiveData<List<TransactionEntity>> = repository.listTransactions - private val _text = MutableLiveData<String>().apply { - value = "This is dashboard Fragment" + fun insertTransaction(transaction: TransactionEntity) = viewModelScope.launch { + repository.insertTransaction(transaction) + } + + fun updateTransaction(transaction: TransactionEntity) = viewModelScope.launch { + repository.updateTransaction(transaction) + } + + fun deleteTransaction(transaction: TransactionEntity) = viewModelScope.launch { + repository.deleteTransaction(transaction) } - val text: LiveData<String> = _text } \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModelFactory.kt b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..d318bade3a0a3dfa99c25021f0e02aac38ab04c7 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionViewModelFactory.kt @@ -0,0 +1,17 @@ +package com.onionsquad.bondoman.ui.transaction + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.onionsquad.bondoman.repository.TransactionRepository + +class TransactionViewModelFactory(private val repository: TransactionRepository) : + ViewModelProvider.Factory { + + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(TransactionViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return TransactionViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt b/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt new file mode 100644 index 0000000000000000000000000000000000000000..ce6beddb309958907a9a4f30e9e9f53511b04ac0 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt @@ -0,0 +1,34 @@ +package com.onionsquad.bondoman.util + +import androidx.room.TypeConverter +import com.onionsquad.bondoman.room.TransactionCategory +import java.util.Date + +object Converters { + + @TypeConverter + @JvmStatic + fun fromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + @JvmStatic + fun dateToTimestamp(value: Date?): Long? { + return value?.time + } + + @TypeConverter + fun fromTransactionCategory(category: TransactionCategory): String { + return category.name + } + + @TypeConverter + fun toTransactionCategory(categoryString: String): TransactionCategory { + return try { + TransactionCategory.valueOf(categoryString) + } catch (e: IllegalArgumentException) { + TransactionCategory.UNKNOWN + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_email_24.xml b/app/src/main/res/drawable/ic_baseline_email_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..a3335d40f9e0e89792c5336c7964bc79e145aa04 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_email_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="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_baseline_save_24.xml b/app/src/main/res/drawable/ic_baseline_save_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..cfd40f5f8e0996658b48908b447dc9886b8ec601 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_save_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="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/> + +</vector> diff --git a/app/src/main/res/layout/activity_add_transaction.xml b/app/src/main/res/layout/activity_add_transaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..95fa2b3af87c278d7b251600cecf1c1b0d0ddacb --- /dev/null +++ b/app/src/main/res/layout/activity_add_transaction.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Transaksi" + android:textSize="24sp" + android:textStyle="bold" /> + + <EditText + android:id="@+id/titleEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:hint="Judul" + android:inputType="text" /> + + <EditText + android:id="@+id/amountEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:hint="Nominal" + android:inputType="numberDecimal" /> + + <RadioGroup + android:id="@+id/categoryRadioGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:orientation="horizontal"> + + <RadioButton + android:id="@+id/incomeRadioButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Pemasukan" /> + + <RadioButton + android:id="@+id/outcomeRadioButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Pengeluaran" /> + + </RadioGroup> + + <EditText + android:id="@+id/locationEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:hint="Lokasi" + android:inputType="text" /> + + <Button + android:id="@+id/saveButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="Simpan" /> + +</LinearLayout> \ 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 index 570a31e7a5eb1be342b0845e3542c86cfe97a81c..b4fa8f9139a6487708df28d49918d4d29402779e 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,18 +1,53 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:orientation="vertical" tools:context=".ui.settings.SettingsFragment"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_save" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="0.7" + android:backgroundTint="@color/white" + android:drawableLeft="@drawable/ic_baseline_save_24" + android:text="@string/action_save_transactions" + android:textAlignment="textStart" + android:textColor="@color/black" /> + + <Spinner + android:id="@+id/spinner_excel_type" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="0.3" /> + </LinearLayout> + <Button - android:id="@+id/logout_button" + android:id="@+id/button_send_email" android:layout_width="match_parent" android:layout_height="wrap_content" + android:backgroundTint="@color/white" + android:drawableLeft="@drawable/ic_baseline_email_24" + android:text="@string/action_save_email" android:textAlignment="textStart" - android:textColor="@color/design_default_color_error" + android:textColor="@color/black" /> + + <Button + android:id="@+id/button_logout" + android:layout_width="match_parent" + android:layout_height="wrap_content" android:backgroundTint="@color/white" android:drawableLeft="@drawable/ic_baseline_logout_24" android:drawableTint="@color/design_default_color_error" - android:text="@string/action_sign_out" /> -</FrameLayout> \ No newline at end of file + android:text="@string/action_sign_out" + android:textAlignment="textStart" + android:textColor="@color/design_default_color_error" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_transaction.xml b/app/src/main/res/layout/fragment_transaction.xml index 57a856060acfaabd0a791420812b73e086ff07f7..26af3dd3316b3d39d8d4a93547724bcb95a87311 100644 --- a/app/src/main/res/layout/fragment_transaction.xml +++ b/app/src/main/res/layout/fragment_transaction.xml @@ -4,4 +4,17 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.transaction.TransactionFragment"/> \ No newline at end of file + tools:context=".ui.transaction.TransactionFragment"> + + <Button + android:id="@+id/button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Button" + android:visibility="visible" + 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/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000000000000000000000000000000000000..df459588e60132333d50587f1090784e62693410 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string-array name="excel_types"> + <item>XLS</item> + <item>XLSX</item> + </string-array> +</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 311f02b1f84e12b270a1b2d85afadf307f9caa25..e48d9841a8bfd416ef39ac9460a98e07c6693f2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,8 @@ <string name="log_out_success">Log out success</string> <string name="yes">Yes</string> <string name="no">No</string> + <string name="action_save_transactions">Save transaction</string> + <string name="action_save_email">Send transactions</string> + <string name="transactions_saved">Transactions saved</string> + <string name="transactions_sent">Transactions sent to email</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..0a9df4b573a6caf941e16d0eb5b2f8cee1ce5cea --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,3 @@ +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path name="cache" path="/" /> +</paths> diff --git a/build.gradle.kts b/build.gradle.kts index a0985efc88dec705956b74f5d6e9ac23c8daebb8..56ccaddfbd0ae102cf1b1f8b9a4ea0727d5445c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false + alias(libs.plugins.jetbrainsKotlinKapt) apply false } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 20e2a01520742aff4ec9ce578ed4fe5f3a76fe77..68c91aec313598d3039f3466d1eca131beb5f553 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,6 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true +android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f97c4635183e36d7327b23cd0961282b2fec0b66..5dc08ee6ecbd46cc587d471484264b8f9153a6c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.3.1" -kotlin = "1.9.0" +agp = "8.2.2" +kotlin = "1.9.23" coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" @@ -12,10 +12,14 @@ lifecycleLivedataKtx = "2.6.1" lifecycleViewmodelKtx = "2.6.1" navigationFragmentKtx = "2.6.0" navigationUiKtx = "2.6.0" +room = "2.6.1" +kapt = "1.9.23" +lifecycleViewmodelCompose = "2.7.0" retrofit = "2.11.0" okhttp3 = "4.12.0" annotation = "1.7.1" work = "2.9.0" +excelkt = "1.0.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -29,14 +33,21 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-common = { group = "androidx.room", name = "room-common", version.ref = "room" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } retrofit2-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit2-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" } retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } +excelkt = { group = "io.github.evanrupert", name = "excelkt", version.ref = "excelkt" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +jetbrainsKotlinKapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kapt" }