diff --git a/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt b/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..e75873f1765de8393edf1c4c5075f5f8391b62c1 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/di/NetworkModule.kt @@ -0,0 +1,55 @@ +package com.informatika.bondoman.di + +import com.informatika.bondoman.BuildConfig +import com.informatika.bondoman.model.remote.AuthService +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import timber.log.Timber + +val networkModule = module { + // Dependency: HttpLoggingInterceptor + single<Interceptor> { + HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + Timber.tag("OkHttp").d(message) + } + }).apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + + // Dependency: OkHttpClient + single { + OkHttpClient.Builder() + .addInterceptor(get<Interceptor>()) + .build() + } + + // Dependency: Moshi + single<Moshi> { + Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + } + + // Dependency: Retrofit + single { + Retrofit.Builder() + .baseUrl(BuildConfig.BASE_URL) + .client(get()) + .addConverterFactory(MoshiConverterFactory.create(get<Moshi>())) + .build() + } + + // Dependency: ApiService + single { + val retrofit: Retrofit = get() + retrofit.create(AuthService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt b/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt index dd0d186dacb6345d2867f7737ce8e4c059fa1be8..6def362937b5d3e1c58a4558cada5ba71313ffe2 100644 --- a/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt +++ b/app/src/main/java/com/informatika/bondoman/model/local/dao/TransactionDao.kt @@ -5,25 +5,27 @@ import androidx.room.Delete import androidx.room.Query import com.informatika.bondoman.model.local.DBConstants import com.informatika.bondoman.model.local.entity.transaction.Category -import com.informatika.bondoman.model.local.entity.transaction.Coordinates import com.informatika.bondoman.model.local.entity.transaction.Transaction @Dao interface TransactionDao { - @Query("SELECT * FROM " + DBConstants.mTableTransaction) + @Query("SELECT * FROM " + DBConstants.mTableTransaction + " WHERE _id = :id") + suspend fun get(id: Int): Transaction + + @Query("SELECT * FROM " + DBConstants.mTableTransaction + " ORDER BY createdAt DESC") suspend fun getAll(): List<Transaction> - @Query("INSERT INTO " + DBConstants.mTableTransaction + " (title, category, amount) VALUES(:title, :category, :amount, :coordinates)") - suspend fun insert(title: String, category: Category, amount: Int, coordinates: Coordinates) + @Query("INSERT INTO " + DBConstants.mTableTransaction + " (title, category, amount, location) VALUES(:title, :category, :amount, :location)") + suspend fun insert(title: String, category: Category, amount: Int, location: String) @Query("INSERT INTO " + DBConstants.mTableTransaction + " (title, category, amount) VALUES(:title, :category, :amount)") suspend fun insert(title: String, category: Category, amount: Int) - @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, category = :category, coordinates = :coordinates") - suspend fun update(title: String, category: Category, coordinates: Coordinates) + @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, amount = :amount, location = :location") + suspend fun update(title: String, amount: Int, location: String) - @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, category = :category") - suspend fun update(title: String, category: Category) + @Query("UPDATE " + DBConstants.mTableTransaction + " SET title = :title, amount = :amount") + suspend fun update(title: String, amount: Int) @Delete suspend fun delete(transaction: Transaction) diff --git a/app/src/main/java/com/informatika/bondoman/model/local/entity/transaction/Transaction.kt b/app/src/main/java/com/informatika/bondoman/model/local/entity/transaction/Transaction.kt index af2449053d746fd863ecf4b8db103e822b916710..0366bc148580310bb7434c4550c3668411b95de8 100644 --- a/app/src/main/java/com/informatika/bondoman/model/local/entity/transaction/Transaction.kt +++ b/app/src/main/java/com/informatika/bondoman/model/local/entity/transaction/Transaction.kt @@ -23,7 +23,6 @@ data class Transaction ( val amount: Int, - @Embedded - val location: Coordinates, + val location: String, ) : Parcelable diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..19492589e7220a6e119239b547ef8ab51144adae --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/model/repository/TokenRepository.kt @@ -0,0 +1,28 @@ +package com.informatika.bondoman.model.repository + +import android.util.Log +import com.informatika.bondoman.network.ApiClient +import com.informatika.bondoman.model.Resource +import retrofit2.awaitResponse +import timber.log.Timber + +class TokenRepository { + suspend fun token(token: String): Resource<Boolean> { + try { + val call = ApiClient.authService.token("Bearer $token") + val response = call.awaitResponse() + + Timber.tag("status").d(response.code().toString()) + + return if (response.isSuccessful) { + Resource.Success(true) // Return success with data + } else if (response.code() == 401) { + Resource.Success(false) + } else { + throw Exception("Error token") + } + } catch (e: Throwable) { + return Resource.Error(e) // Return error with throwable + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt b/app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc4c07462c52d8c5e24c306ae5e183cd25392f23 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/model/repository/TransactionRepository.kt @@ -0,0 +1,70 @@ +package com.informatika.bondoman.model.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.local.dao.TransactionDao +import com.informatika.bondoman.model.local.entity.transaction.Category +import com.informatika.bondoman.model.local.entity.transaction.Transaction + +class TransactionRepository(private var transactionDao: TransactionDao) { + private val _listTransactionLiveData = MutableLiveData<Resource<List<Transaction>>>() + private val listTransactionLiveData : LiveData<Resource<List<Transaction>>> = _listTransactionLiveData + + private val _transactionLiveData = MutableLiveData<Resource<Transaction>>() + private val transactionLiveData : LiveData<Resource<Transaction>> = _transactionLiveData + + suspend fun getTransaction(id: Int) { + _transactionLiveData.postValue(Resource.Loading()) + try { + val transaction = transactionDao.get(id) + _transactionLiveData.postValue(Resource.Success(transaction)) + } catch (e: Exception) { + _transactionLiveData.postValue(Resource.Error(e)) + } + } + + suspend fun getAllTransaction() { + _listTransactionLiveData.postValue(Resource.Loading()) + try { + val transactionList = transactionDao.getAll() + _listTransactionLiveData.postValue(Resource.Success(transactionList)) + } catch (e: Exception) { + _listTransactionLiveData.postValue(Resource.Error(e)) + } + } + + suspend fun insertTransaction(title: String, category: Category, amount: Int, location: String) { + transactionDao.insert(title, category, amount, location) + } + + suspend fun insertTransaction(title: String, category: Category, amount: Int) { + transactionDao.insert(title, category, amount) + } + + suspend fun updateTransaction(title: String, amount: Int, location: String) { + transactionDao.update(title, amount, location) + } + + suspend fun updateTransaction(title: String, amount: Int) { + transactionDao.update(title, amount) + } + + suspend fun deleteTransaction(transaction: Transaction) { + transactionDao.delete(transaction) + } + + suspend fun refreshTransaction() { + _listTransactionLiveData.postValue(Resource.Loading()) + try { + val transactionList = transactionDao.getAll() + _listTransactionLiveData.postValue(Resource.Success(transactionList)) + } catch (e: Exception) { + _listTransactionLiveData.postValue(Resource.Error(e)) + } + } + + fun getListTransactionLiveData() = listTransactionLiveData + + fun getTransactionLiveData() = transactionLiveData +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/repository/TokenRepository.kt b/app/src/main/java/com/informatika/bondoman/repository/TokenRepository.kt deleted file mode 100644 index 6ba3cb5f857253ed3e4b9a709261960d6c612bad..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/informatika/bondoman/repository/TokenRepository.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.informatika.bondoman.repository - -import android.util.Log -import com.informatika.bondoman.network.ApiClient -import com.informatika.bondoman.network.ApiResponse -import retrofit2.awaitResponse - -class TokenRepository { - suspend fun token(token: String): ApiResponse<Boolean> { - try { - val call = ApiClient.authService.token(token) - val response = call.awaitResponse() - - Log.d("status", response.code().toString()) - - return if (response.isSuccessful) { - ApiResponse.Success(true) // Return success with data - } else if (response.code() == 401) { - ApiResponse.Success(false) - } else { - throw Exception("Error token") - } - } catch (e: Throwable) { - return ApiResponse.Error(Exception("Error token", e)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt b/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt index 4aca21c8402a3d6599244be9a1dd06a3c0875168..9e732be57ea30f1a21c1a8f3e4db9f76caa570df 100644 --- a/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt +++ b/app/src/main/java/com/informatika/bondoman/view/activity/MainEmptyActivity.kt @@ -7,11 +7,17 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope +import com.informatika.bondoman.BuildConfig import com.informatika.bondoman.MainActivity import com.informatika.bondoman.databinding.ActivityMainEmptyBinding -import com.informatika.bondoman.view.activity.login.LoginActivity +import com.informatika.bondoman.di.databaseModule +import com.informatika.bondoman.di.networkModule +import com.informatika.bondoman.di.viewModelModule import com.informatika.bondoman.prefdatastore.JWTManager import kotlinx.coroutines.launch +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import timber.log.Timber class MainEmptyActivity : AppCompatActivity() { private lateinit var jwtManager: JWTManager @@ -21,6 +27,9 @@ class MainEmptyActivity : AppCompatActivity() { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + initTimber() + initKoin() + binding = ActivityMainEmptyBinding.inflate(layoutInflater) setContentView(binding.root) @@ -29,7 +38,6 @@ class MainEmptyActivity : AppCompatActivity() { installSplashScreen() enableEdgeToEdge() - // Hide the header bar jwtManager = JWTManager(applicationContext) lifecycleScope.launch { @@ -47,4 +55,23 @@ class MainEmptyActivity : AppCompatActivity() { } } } + + private fun initKoin() { + startKoin { + modules( + listOf( + databaseModule, + networkModule, + viewModelModule + ) + ) + } + } + + private fun initTimber() { + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt b/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt index fa83038857364b6b68861db51b4d8942d3889613..9cfd7105084e731ac1bc1ef76015662c707d7bbe 100644 --- a/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt +++ b/app/src/main/java/com/informatika/bondoman/view/fragment/TransactionFragment.kt @@ -7,7 +7,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels import com.informatika.bondoman.R -import com.informatika.bondoman.viewmodel.TransactionViewModel +import com.informatika.bondoman.viewmodel.transaction.ListTransactionViewModel class TransactionFragment : Fragment() { @@ -15,7 +15,7 @@ class TransactionFragment : Fragment() { fun newInstance() = TransactionFragment() } - private val viewModel: TransactionViewModel by viewModels() + private val viewModel: ListTransactionViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/TransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/TransactionViewModel.kt deleted file mode 100644 index 1bccc6013316772464ef16b091071d36fef04b2c..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/informatika/bondoman/viewmodel/TransactionViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.informatika.bondoman.viewmodel - -import androidx.lifecycle.ViewModel - -class TransactionViewModel : ViewModel() { - // TODO: Implement the ViewModel -} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..e8296e5b8eb9700e74aed1007de5f22a156af15a --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/CreateTransactionViewModel.kt @@ -0,0 +1,66 @@ +package com.informatika.bondoman.viewmodel.transaction + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.informatika.bondoman.R +import com.informatika.bondoman.model.local.entity.transaction.Category +import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.viewmodel.transaction.helper.TransactionFormState +import kotlinx.coroutines.launch + +class CreateTransactionViewModel(private var transactionRepository: TransactionRepository) : ViewModel() { + private val _createTransactionForm = MutableLiveData<TransactionFormState>() + val createTransactionFormState: LiveData<TransactionFormState> = _createTransactionForm + + fun createTransaction( + title: String, + category: Category, + amount: Int, + location: String? + ) { + viewModelScope.launch { + if (location != null) { + transactionRepository.insertTransaction(title, category, amount, location) + } else { + transactionRepository.insertTransaction(title, category, amount) + } + } + } + + fun createTransactionDataChanged(title: String, amount: String, category: String) { + if (!isTitleValid(title)) { + _createTransactionForm.value = TransactionFormState(titleError = R.string.invalid_title) + } else if (isAmountValid(amount) > 0) { + val amountError = when (isAmountValid(amount)) { + 1 -> R.string.invalid_amount_empty + 2 -> R.string.invalid_amount_negative + else -> null + } + _createTransactionForm.value = TransactionFormState(amountError = amountError) + } else if (!isCategoryValid(category)) { + _createTransactionForm.value = TransactionFormState(categoryError = R.string.invalid_category) + } else { + _createTransactionForm.value = TransactionFormState(isDataValid = true) + } + } + + private fun isTitleValid(title: String): Boolean { + return title.isNotBlank() + } + + private fun isAmountValid(amount: String): Int { + return if (amount.isBlank()) { + 1 + } else if (amount.toInt() <= 0) { + 2 + } else { + 0 + } + } + + private fun isCategoryValid(category: String = ""): Boolean { + return category.isNotBlank() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..de71945aa0b0007158e459e7cf28d9d9c683e0f7 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/DetailTransactionViewModel.kt @@ -0,0 +1,28 @@ +package com.informatika.bondoman.viewmodel.transaction + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.local.entity.transaction.Transaction +import com.informatika.bondoman.model.repository.TransactionRepository +import kotlinx.coroutines.launch + +class DetailTransactionViewModel(private var transactionRepository: TransactionRepository, transaction: Transaction) : ViewModel() { + val transactionLiveData = MutableLiveData<Resource<Transaction>>() + private val observer = androidx.lifecycle.Observer<Resource<Transaction>> { + transactionLiveData.postValue(it) + } + + init { + transactionRepository.getTransactionLiveData().observeForever(observer) + viewModelScope.launch { + transactionRepository.getTransaction(transaction._id) + } + } + + override fun onCleared() { + super.onCleared() + transactionRepository.getTransactionLiveData().removeObserver(observer) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..279d0e1e07c4335e8f6095583107863e0b987579 --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/ListTransactionViewModel.kt @@ -0,0 +1,33 @@ +package com.informatika.bondoman.viewmodel.transaction + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.local.entity.transaction.Transaction +import com.informatika.bondoman.model.repository.TransactionRepository +import kotlinx.coroutines.launch + +class ListTransactionViewModel(private var transactionRepository: TransactionRepository) : ViewModel() { + val listTransactionLiveData = MutableLiveData<Resource<List<Transaction>>>() + private val observer = androidx.lifecycle.Observer<Resource<List<Transaction>>> { + listTransactionLiveData.postValue(it) + } + + init { + transactionRepository.getListTransactionLiveData().observeForever(observer) + } + + fun getAllTransaction() { + viewModelScope.launch { + transactionRepository.getAllTransaction() + } + } + + override fun onCleared() { + super.onCleared() + transactionRepository.getListTransactionLiveData().removeObserver(observer) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..16da0cb251f8038ea0a46eb038429c5ab6eea4fd --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/UpdateTransactionViewModel.kt @@ -0,0 +1,31 @@ +package com.informatika.bondoman.viewmodel.transaction + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.informatika.bondoman.model.Resource +import com.informatika.bondoman.model.local.entity.transaction.Transaction +import com.informatika.bondoman.model.repository.TransactionRepository +import com.informatika.bondoman.viewmodel.transaction.helper.TransactionFormState +import kotlinx.coroutines.launch + +class UpdateTransactionViewModel(private var transactionRepository: TransactionRepository, transaction: Transaction) : ViewModel() { + val transactionLiveData = MutableLiveData<Resource<Transaction>>() + private val observer = androidx.lifecycle.Observer<Resource<Transaction>> { + transactionLiveData.postValue(it) + } + + private val _updateTransactionForm = MutableLiveData<TransactionFormState>() + + init { + transactionRepository.getTransactionLiveData().observeForever(observer) + viewModelScope.launch { + transactionRepository.getTransaction(transaction._id) + } + } + + override fun onCleared() { + super.onCleared() + transactionRepository.getTransactionLiveData().removeObserver(observer) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/helper/TransactionFormState.kt b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/helper/TransactionFormState.kt new file mode 100644 index 0000000000000000000000000000000000000000..d09844478ac1456830fcc2802ecacc250fc5511c --- /dev/null +++ b/app/src/main/java/com/informatika/bondoman/viewmodel/transaction/helper/TransactionFormState.kt @@ -0,0 +1,8 @@ +package com.informatika.bondoman.viewmodel.transaction.helper + +data class TransactionFormState ( + val titleError: Int? = null, + val amountError: Int? = null, + val categoryError: Int? = null, + val isDataValid: Boolean = false +) \ 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 f00b1699413b59d568274f9f91c34f1cfad85635..668474f63c49924906b39e4e86f66224753a7691 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,12 +7,34 @@ <string name="title_settings">Settings</string> <string name="title_activity_login">Login</string> <string name="title_login">Masuk</string> + <string name="prompt_email">Email</string> <string name="prompt_password">Password</string> <string name="action_sign_in">Sign in</string> <string name="action_sign_in_short">Sign in</string> - <string name="welcome">"Welcome!"</string> <string name="invalid_email">Not a valid email</string> <string name="invalid_password">Password must be >5 characters</string> + <string name="welcome">"Welcome!"</string> <string name="login_failed">"Login failed"</string> + + <string name="prompt_title">"Title"</string> + <string name="prompt_category">"Category"</string> + <string name="prompt_amount">"Amount"</string> + <string name="action_save">"Save"</string> + <string name="action_cancel">"Cancel"</string> + <string name="action_delete">"Delete"</string> + <string name="action_edit">"Edit"</string> + <string name="action_add">"+"</string> + <string name="invalid_title">"Fill the title!"</string> + <string name="invalid_amount_empty">"Fill the amount!"</string> + <string name="invalid_amount_negative">"Amount must be positive!"</string> + <string name="invalid_category">"Choose the category!"</string> + <string name="transaction_saved">"Transaction saved!"</string> + <string name="transaction_deleted">"Transaction deleted!"</string> + <string name="transaction_updated">"Transaction updated!"</string> + <string name="transaction_not_saved">"Transaction not saved!"</string> + <string name="transaction_not_deleted">"Transaction not deleted!"</string> + <string name="transaction_not_updated">"Transaction not updated!"</string> + <string name="transaction_not_found">"Transaction not found!"</string> + </resources> \ No newline at end of file