diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b0bdf7f7567038f51635a4452b86bd290d3113c..c20a2f04f868d614c9066e7a5f35c9a9c8e0c5a2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,6 +34,12 @@ android { buildFeatures { viewBinding = true } + + packaging { + resources { + excludes += "META-INF/*" + } + } } dependencies { @@ -47,6 +53,7 @@ dependencies { 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) @@ -60,4 +67,16 @@ dependencies { // Chart implementation(libs.mpandroidchart) + + // Retrofit + implementation(libs.retrofit2.retrofit) + implementation(libs.retrofit2.converter.gson) + implementation(libs.retrofit2.converter.scalars) + implementation(libs.okhttp3) + + // 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 497e69a30d2e3148e20ee25c48d931ee8caeb56e..4dd5ff7aea82efe33a729eefc3636fb98bb6e934 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools"> + xmlns:tools="http://schemas.android.com/tools" > + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> <application android:allowBackup="true" @@ -10,11 +15,19 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Bondoman" - > + android:theme="@style/Theme.Bondoman" > + <activity + android:name=".NoInternetActivity" + android:exported="false" + android:noHistory="true" + android:theme="@style/Theme.Bondoman" /> + <activity + android:name=".ui.login.LoginActivity" + android:exported="false" + android:label="@string/title_activity_login"/> <activity android:name=".MainActivity" - android:exported="true"> + android:exported="true" > <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -23,7 +36,21 @@ </activity> <activity android:name=".AddTransactionActivity" - android:exported="false"/> + android:exported="false" /> + + <activity + android:name=".EditTransactionActivity" + 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 index 34088743ebf42d06a2a3dcd0815421bae4aedc49..e49b61b1892a12f7b0a11a5ec1e4386ab37c519f 100644 --- a/app/src/main/java/com/onionsquad/bondoman/AddTransactionActivity.kt +++ b/app/src/main/java/com/onionsquad/bondoman/AddTransactionActivity.kt @@ -2,11 +2,11 @@ package com.onionsquad.bondoman import android.content.Intent import android.os.Bundle -import android.widget.Button -import android.widget.EditText 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 @@ -14,30 +14,29 @@ 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) - setContentView(R.layout.activity_add_transaction) - - val titleEditText: EditText = findViewById(R.id.titleEditText) - val amountEditText: EditText = findViewById(R.id.amountEditText) - val categoryEditText: EditText = findViewById(R.id.categoryEditText) - val locationEditText: EditText = findViewById(R.id.locationEditText) + binding = ActivityAddTransactionBinding.inflate(layoutInflater) + setContentView(binding.root) val factory = TransactionViewModelFactory(repository) val viewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] - val saveButton: Button = findViewById(R.id.saveButton) - saveButton.setOnClickListener { - val title = titleEditText.text.toString() - val amount = amountEditText.text.toString().toDoubleOrNull() ?: 0.0 - val category = categoryEditText.text.toString() - val location = locationEditText.text.toString() + 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() - // Dapatkan tanggal saat ini val currentDate = Date() val transaction = TransactionEntity( @@ -50,8 +49,6 @@ class AddTransactionActivity : AppCompatActivity() { viewModel.insertTransaction(transaction) - - // Navigasi kembali ke halaman daftar transaksi Intent(this@AddTransactionActivity, MainActivity::class.java).also { startActivity(it) } diff --git a/app/src/main/java/com/onionsquad/bondoman/EditTransactionActivity.kt b/app/src/main/java/com/onionsquad/bondoman/EditTransactionActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..5606ede65b8da631bc51b5024b1e7c5ff9ef8537 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/EditTransactionActivity.kt @@ -0,0 +1,79 @@ +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.ActivityEditTransactionBinding +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 EditTransactionActivity : AppCompatActivity() { + private lateinit var binding: ActivityEditTransactionBinding + private val database by lazy { TransactionDatabase.getInstance(this) } + private val repository by lazy { TransactionRepository(database.transactionDao()) } + private lateinit var viewModel: TransactionViewModel + + private var transactionId: Int = -1 + private var transactionDate: Date = Date() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityEditTransactionBinding.inflate(layoutInflater) + setContentView(binding.root) + + val factory = TransactionViewModelFactory(repository) + viewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] + + transactionId = intent.getIntExtra("transactionId", -1) + if (transactionId != -1) { + viewModel.getTransactionById(transactionId).observe(this) { transaction -> + if (transaction != null) { + binding.titleEditText.setText(transaction.title) + binding.amountEditText.setText(transaction.amount.toString()) + when (transaction.category) { + TransactionCategory.INCOME -> binding.incomeRadioButton.isChecked = true + TransactionCategory.OUTCOME -> binding.outcomeRadioButton.isChecked = true + else -> { + + } + } + binding.locationEditText.setText(transaction.location) + transactionDate = transaction.date + } + } + } + + 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 updatedTransaction = TransactionEntity( + id = transactionId, + title = title, + amount = amount, + category = category, + date = transactionDate, + location = location + ) + + viewModel.updateTransaction(updatedTransaction) + + Intent(this@EditTransactionActivity, MainActivity::class.java).also { + startActivity(it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/InternetActivity.kt b/app/src/main/java/com/onionsquad/bondoman/InternetActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..34eb4e6274ac76be39f1ea50296ab38041e77fb1 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/InternetActivity.kt @@ -0,0 +1,44 @@ +package com.onionsquad.bondoman + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Bundle +import android.os.PersistableBundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity + +open class InternetActivity : AppCompatActivity() { + + var isInternetConnected = false + private set + + private val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + isInternetConnected = true + } + + override fun onLost(network: Network) { + super.onLost(network) + isInternetConnected = false + Toast.makeText( + this@InternetActivity, + R.string.internet_not_connected, + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager.requestNetwork(networkRequest, networkCallback) + } +} \ 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 4212af32fbcabcdf8f4c3758bdf7d5ec4ca2df2e..e8285f954ad06c9e3d7c7745ded7efa6d477c647 100644 --- a/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt +++ b/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt @@ -1,5 +1,6 @@ package com.onionsquad.bondoman +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.findNavController @@ -7,15 +8,23 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView +import com.onionsquad.bondoman.auth.AutoLogoutWorker +import com.onionsquad.bondoman.auth.SessionManager import com.onionsquad.bondoman.databinding.ActivityMainBinding +import com.onionsquad.bondoman.ui.login.LoginActivity -class MainActivity : AppCompatActivity() { +class MainActivity : InternetActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + AutoLogoutWorker.start(this) + + val sessionManager = SessionManager(this) + sessionManager.ensureAuthenticated() + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/java/com/onionsquad/bondoman/NoInternetActivity.kt b/app/src/main/java/com/onionsquad/bondoman/NoInternetActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..c17e7959a6d92b4be05ee8c321ff3e198b0550ff --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/NoInternetActivity.kt @@ -0,0 +1,17 @@ +package com.onionsquad.bondoman + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.onionsquad.bondoman.databinding.ActivityNoInternetBinding + +class NoInternetActivity : AppCompatActivity() { + + private lateinit var binding: ActivityNoInternetBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityNoInternetBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/auth/AutoLogoutWorker.kt b/app/src/main/java/com/onionsquad/bondoman/auth/AutoLogoutWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..bbf7a225b6c8024633518557dbfff5b6b2eaa713 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/auth/AutoLogoutWorker.kt @@ -0,0 +1,58 @@ +package com.onionsquad.bondoman.auth + +import android.content.Context +import android.util.Log +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import java.time.Instant +import java.util.concurrent.TimeUnit +import kotlin.math.max + + +class AutoLogoutWorker( + private val context: Context, + private val workerParameters: WorkerParameters +) : Worker(context, workerParameters) { + + override fun doWork(): Result { + Log.d(this::class.java.simpleName, "Proceed auto logout") + val sessionManager = SessionManager(context) + sessionManager.deleteAuthToken() + return Result.success() + } + + companion object { + fun start(context: Context) { + Log.d(AutoLogoutWorker::class.java.simpleName, "Starting auto logout worker") + val sessionManager = SessionManager(context) + val token = sessionManager.fetchAuthToken() + if (token != null) { + val duration = max(0, token.exp - Instant.now().epochSecond) + Log.d(AutoLogoutWorker::class.java.simpleName, "Proceed to logout in $duration secs") + val workRequest = OneTimeWorkRequestBuilder<AutoLogoutWorker>() + .setInitialDelay(duration, TimeUnit.SECONDS) + .build() + WorkManager + .getInstance(context) + .enqueueUniqueWork( + AutoLogoutWorker::class.java.simpleName, + ExistingWorkPolicy.REPLACE, + workRequest + ) + } else { + stop(context) + } + } + + fun stop(context: Context) { + Log.d(AutoLogoutWorker::class.java.simpleName, "Stopping auto logout worker") + WorkManager + .getInstance(context) + .cancelUniqueWork(AutoLogoutWorker::class.java.simpleName) + } + } + +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..1089d87fad8930e1e229c7f55109bbc33d601161 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/auth/SessionManager.kt @@ -0,0 +1,41 @@ +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(private val context: Context) { + private val sharedPreferences = context.getSharedPreferences( + context.getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ) + + companion object { + const val USER_TOKEN = "user_token" + } + + fun fetchAuthToken(): Token? { + val tokenJson = sharedPreferences.getString(USER_TOKEN, null) + return if (tokenJson != null) Gson().fromJson(tokenJson, Token::class.java) else null + } + + fun saveAuthToken(token: Token) { + sharedPreferences.edit().putString(USER_TOKEN, Gson().toJson(token)).apply() + Log.d("TOKEN", "${fetchAuthToken()}") + } + + 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/auth/Token.kt b/app/src/main/java/com/onionsquad/bondoman/auth/Token.kt new file mode 100644 index 0000000000000000000000000000000000000000..435b5dba0a1767c56fbcaa233912f555fb34b0c8 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/auth/Token.kt @@ -0,0 +1,3 @@ +package com.onionsquad.bondoman.auth + +data class Token(val tokenString: String, val nim: String, val exp: Long, val iat: Long) diff --git a/app/src/main/java/com/onionsquad/bondoman/network/BondomanApiService.kt b/app/src/main/java/com/onionsquad/bondoman/network/BondomanApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ecec9762bcbbb22be1f663763a71a9c8b18343b --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/network/BondomanApiService.kt @@ -0,0 +1,36 @@ +package com.onionsquad.bondoman.network + +import okhttp3.OkHttpClient +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.POST + + +private const val BASE_URL = "https://pbd-backend-2024.vercel.app/" +private val client = OkHttpClient.Builder().build() +private val retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + +interface BondomanApiService { + @Headers("Content-Type: application/json") + @POST("api/auth/login") + fun login(@Body loginData: LoginRequest): Call<String> + + @POST("api/auth/token") + fun token(@Header("Authorization") token: String): Call<TokenResponse> +} + +object BondomanApi { + val retrofitService: BondomanApiService by lazy { + retrofit.create(BondomanApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/network/LoginRequest.kt b/app/src/main/java/com/onionsquad/bondoman/network/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..c2a6d36c6bc1bf96f6c3af2aee461c9c78946eec --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/network/LoginRequest.kt @@ -0,0 +1,3 @@ +package com.onionsquad.bondoman.network + +data class LoginRequest(val email: String?, val password: String?) \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/network/LoginResponse.kt b/app/src/main/java/com/onionsquad/bondoman/network/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..c264da3200c401f27b1c04755eb4aa154d44bbb1 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/network/LoginResponse.kt @@ -0,0 +1,5 @@ +package com.onionsquad.bondoman.network + +import com.google.gson.annotations.SerializedName + +data class LoginResponse(val token: String?) diff --git a/app/src/main/java/com/onionsquad/bondoman/network/TokenResponse.kt b/app/src/main/java/com/onionsquad/bondoman/network/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..ed2cc004cb0bcc9941a021053d1a92fb622eab8b --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/network/TokenResponse.kt @@ -0,0 +1,3 @@ +package com.onionsquad.bondoman.network + +data class TokenResponse(val nim: String?, val exp: Long?, val iat: Long?) diff --git a/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt index ac46a924739ec3fe9c2e64c6773d2025fdd85d17..584f0e36d7b8a05e698c858cc62e440d02539ef0 100644 --- a/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt +++ b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt @@ -19,4 +19,8 @@ class TransactionRepository(private val transactionDao: TransactionDao) { suspend fun deleteTransaction(transaction: TransactionEntity) { transactionDao.deleteTransaction(transaction) } + + fun getTransactionById(id: Int): LiveData<TransactionEntity> { + return transactionDao.getTransactionById(id) + } } \ 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 index 31e75bd6384c1a356ccaa4c5b4939af37434a8f9..601fb1c31a914a5f7f355f08dea8b2751c5995eb 100644 --- a/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt @@ -22,4 +22,7 @@ interface TransactionDao { @Query("SELECT * FROM transactions WHERE category = 'OUTCOME'") fun getAllOutcomes(): LiveData<List<TransactionEntity>> + + @Query("SELECT * FROM transactions WHERE id = :id") + fun getTransactionById(id: Int): LiveData<TransactionEntity> } \ 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 index 66c176602c7f51214ba4ddd47de96c2ae911e837..64898cbf7c8ebf97163227d9ddd15a23450d0bc8 100644 --- a/app/src/main/java/com/onionsquad/bondoman/room/TransactionEntity.kt +++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionEntity.kt @@ -4,11 +4,11 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.ColumnInfo import androidx.room.TypeConverters -import com.onionsquad.bondoman.util.DateConverter +import com.onionsquad.bondoman.util.Converters import java.util.Date @Entity(tableName = "transactions") -@TypeConverters(DateConverter::class) +@TypeConverters(Converters::class) data class TransactionEntity( @PrimaryKey(autoGenerate = true) val id: Int = 0, @@ -20,7 +20,7 @@ data class TransactionEntity( val amount: Double, @ColumnInfo(name = "category") - val category: String, + val category: TransactionCategory, @ColumnInfo(name = "date", defaultValue = "CURRENT_TIMESTAMP") val date: Date, diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginActivity.kt b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..2774d6bf67bd5c8c4385e964a7d047f1fe3d4a7d --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginActivity.kt @@ -0,0 +1,149 @@ +package com.onionsquad.bondoman.ui.login + +import android.app.Activity +import android.content.Intent +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.Toast +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.onionsquad.bondoman.InternetActivity +import com.onionsquad.bondoman.MainActivity +import com.onionsquad.bondoman.NoInternetActivity +import com.onionsquad.bondoman.databinding.ActivityLoginBinding + +import com.onionsquad.bondoman.R +import com.onionsquad.bondoman.auth.AutoLogoutWorker +import com.onionsquad.bondoman.auth.SessionManager +import java.time.Instant +import java.util.concurrent.TimeUnit + +class LoginActivity : InternetActivity() { + + private lateinit var loginViewModel: LoginViewModel + private lateinit var binding: ActivityLoginBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + val email = binding.username + val password = binding.password + val login = binding.login + val loading = binding.loading + + val sessionManager = SessionManager(this) + + loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java] + .apply { + loginFormState.observe(this@LoginActivity, Observer { + val loginState = it ?: return@Observer + + login.isEnabled = loginState.isDataValid + + if (loginState.emailError != null) { + email.error = getString(loginState.emailError) + } + }) + loginResult.observe(this@LoginActivity, Observer { + val loginResult = it ?: return@Observer + + loading.visibility = View.GONE + if (loginResult.error != null) { + showLoginFailed(loginResult.error) + } + if (loginResult.success != null) { + sessionManager.saveAuthToken(loginResult.success) + AutoLogoutWorker.start(this@LoginActivity) + showLoginSuccess() + sendToMainActivity() + } + }) + } + + email.afterTextChanged { + loginViewModel.loginDataChanged( + email.text.toString(), + password.text.toString() + ) + } + + password.apply { + afterTextChanged { + loginViewModel.loginDataChanged( + email.text.toString(), + password.text.toString() + ) + } + + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + if (!isInternetConnected) { + sendToNoInternetActivity() + } else { + loginViewModel.login( + email.text.toString(), + password.text.toString() + ) + } + } + } + false + } + + login.setOnClickListener { + if (!isInternetConnected) { + sendToNoInternetActivity() + return@setOnClickListener + } else { + loading.visibility = View.VISIBLE + loginViewModel.login(email.text.toString(), password.text.toString()) + } + } + } + } + + private fun sendToNoInternetActivity() { + val intent = Intent(this@LoginActivity, NoInternetActivity::class.java) + startActivity(intent) + } + + private fun sendToMainActivity() { + val intent = Intent(this@LoginActivity, MainActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startActivity(intent) + finish() + } + + private fun showLoginSuccess() { + Toast.makeText(applicationContext, R.string.login_success, Toast.LENGTH_LONG).show() + } + + private fun showLoginFailed(error: String) { + Toast.makeText(applicationContext, error, Toast.LENGTH_SHORT).show() + } +} + +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginFormState.kt b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginFormState.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc8eb07260f3c148cec77ef1264538bf0588ed97 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginFormState.kt @@ -0,0 +1,6 @@ +package com.onionsquad.bondoman.ui.login + +data class LoginFormState( + val emailError: Int? = null, + val isDataValid: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginResult.kt b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..25ce877ebd7c45ae4ca479ce9be4a4aad7c71ed5 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginResult.kt @@ -0,0 +1,8 @@ +package com.onionsquad.bondoman.ui.login + +import com.onionsquad.bondoman.auth.Token + +data class LoginResult( + val success: Token? = null, + val error: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginViewModel.kt b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..6cb095fbcddae41cb7ce9b7f78e243d2ea8766d3 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/login/LoginViewModel.kt @@ -0,0 +1,87 @@ +package com.onionsquad.bondoman.ui.login + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import android.util.Patterns +import com.google.gson.Gson + +import com.onionsquad.bondoman.R +import com.onionsquad.bondoman.auth.SessionManager +import com.onionsquad.bondoman.auth.Token +import com.onionsquad.bondoman.network.BondomanApi +import com.onionsquad.bondoman.network.LoginRequest +import com.onionsquad.bondoman.network.LoginResponse +import com.onionsquad.bondoman.network.TokenResponse +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class LoginViewModel : ViewModel() { + + private val _loginForm = MutableLiveData<LoginFormState>() + val loginFormState: LiveData<LoginFormState> = _loginForm + + private val _loginResult = MutableLiveData<LoginResult>() + val loginResult: LiveData<LoginResult> = _loginResult + + fun login(email: String, password: String) { + BondomanApi.retrofitService.login(LoginRequest(email, password)).enqueue( + object : Callback<String> { + override fun onResponse(call: Call<String>, response: Response<String>) { + if (response.isSuccessful) { + val body = response.body() + val tokenString = Gson().fromJson(body, LoginResponse::class.java).token!! + Log.d("TOKEN", tokenString) + fetchTokenInformation(tokenString) + } else { + val errBody = response.errorBody()!!.string() + val error = when { + errBody.contains("email") -> "Invalid email" + errBody.contains("password") -> "Invalid password" + else -> "Invalid email or password" + } + _loginResult.value = LoginResult(error = error) + } + } + + override fun onFailure(call: Call<String>, t: Throwable) { + _loginResult.value = LoginResult(error = t.message) + } + } + ) + } + + private fun fetchTokenInformation(tokenString: String) { + BondomanApi.retrofitService.token("Bearer $tokenString").enqueue( + object : Callback<TokenResponse> { + override fun onResponse( + call: Call<TokenResponse>, + response: Response<TokenResponse> + ) { + val token = response.body()!! + _loginResult.value = LoginResult( + success = Token(tokenString, token.nim!!, token.exp!!, token.iat!!) + ) + } + + override fun onFailure(call: Call<TokenResponse>, t: Throwable) { + _loginResult.value = LoginResult(error = t.message) + } + } + ) + } + + fun loginDataChanged(email: String, password: String) { + if (!isEmailValid(email)) { + _loginForm.value = LoginFormState(emailError = R.string.invalid_email) + } else { + _loginForm.value = LoginFormState(isDataValid = true) + } + } + + private fun isEmailValid(username: String): Boolean { + return Patterns.EMAIL_ADDRESS.matcher(username).matches() + } +} \ No newline at end of file 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 dc153a26a1d0a02e397c9400600979bbc3a8be75..1219bbe14f7a5a8503001852174bbf81f0539ad8 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,35 +1,220 @@ package com.onionsquad.bondoman.ui.settings +import android.app.AlertDialog +import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.core.content.FileProvider import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import com.onionsquad.bondoman.InternetActivity +import com.onionsquad.bondoman.NoInternetActivity +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 class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null - // This property is only valid between onCreateView and - // onDestroyView. 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?, savedInstanceState: Bundle? ): View { - val transactionViewModel = - ViewModelProvider(this).get(SettingsViewModel::class.java) - _binding = FragmentSettingsBinding.inflate(inflater, container, false) + val sessionManager = SessionManager(requireContext()) + + binding.apply { + buttonLogout.setOnClickListener { + val alertBuilder = AlertDialog.Builder(requireContext()) + alertBuilder.setTitle(R.string.title_alert_logout) + alertBuilder.setMessage(R.string.message_alert_logout) + alertBuilder.setPositiveButton(R.string.yes) { _, _ -> + AutoLogoutWorker.stop(requireContext()) + logout() + } + alertBuilder.setNegativeButton(R.string.no) { dialog, _ -> + dialog.cancel() + } + 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 { + if (!(requireActivity() as InternetActivity).isInternetConnected) { + sendToNoInternetActivity() + return@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 } + private fun sendToNoInternetActivity() { + val intent = Intent(requireContext(), NoInternetActivity::class.java) + startActivity(intent) + } + override fun onDestroyView() { super.onDestroyView() _binding = null } + + private fun logout() { + val sessionManager = SessionManager(requireContext()) + sessionManager.deleteAuthToken() + Toast.makeText(requireContext(), R.string.log_out_success, Toast.LENGTH_SHORT).show() + 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/settings/SettingsViewModel.kt b/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsViewModel.kt deleted file mode 100644 index a877767c5378529d02be28a4688b0589b13d6064..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsViewModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.onionsquad.bondoman.ui.settings - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class SettingsViewModel : 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/com/onionsquad/bondoman/ui/transaction/TransactionAdapter.kt b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..b39a926ac4c347ae5a3e08f8d1763d99622c9af9 --- /dev/null +++ b/app/src/main/java/com/onionsquad/bondoman/ui/transaction/TransactionAdapter.kt @@ -0,0 +1,53 @@ +package com.onionsquad.bondoman.ui.transaction + +import android.content.Intent +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.onionsquad.bondoman.EditTransactionActivity +import com.onionsquad.bondoman.databinding.TransactionCardBinding +import com.onionsquad.bondoman.room.TransactionEntity + +class TransactionAdapter( + private val viewModel: TransactionViewModel, + private var transactionList: List<TransactionEntity> = emptyList() +) : RecyclerView.Adapter<TransactionAdapter.ViewHolder>() { + + inner class ViewHolder(private val binding: TransactionCardBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(transaction: TransactionEntity) { + binding.titleTextView.text = transaction.title + binding.amountTextView.text = "IDR ".plus(transaction.amount) + binding.categoryTextView.text = transaction.category.toString() + binding.dateTextView.text = transaction.date.toString() + binding.locationTextView.text = transaction.location + + binding.deleteButton.setOnClickListener { + viewModel.deleteTransaction(transaction) + } + binding.editButton.setOnClickListener { + val intent = Intent(binding.root.context, EditTransactionActivity::class.java) + intent.putExtra("transactionId", transaction.id) + binding.root.context.startActivity(intent) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = TransactionCardBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val transaction = transactionList[position] + holder.bind(transaction) + } + + override fun getItemCount(): Int { + return transactionList.size + } + + fun setTransactionList(transactionList: List<TransactionEntity>) { + this.transactionList = transactionList + notifyDataSetChanged() + } +} \ 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 4ff1ccc32f50abe95a0e0e74e85e2595bf8997e6..7af3c31b9b13576aaee461b52885ff04ae8710f7 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 @@ -7,38 +7,60 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager 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() { - private var _binding: FragmentTransactionBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! private val database by lazy { TransactionDatabase.getInstance(requireContext().applicationContext) } private val repository by lazy { TransactionRepository(database.transactionDao()) } + private lateinit var transactionViewModel: TransactionViewModel + private lateinit var transactionAdapter: TransactionAdapter override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { + _binding = FragmentTransactionBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupViewModel() + setupRecyclerView() + setupClickListeners() + } + + private fun setupViewModel() { val factory = TransactionViewModelFactory(repository) - val transactionViewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] + transactionViewModel = ViewModelProvider(this, factory)[TransactionViewModel::class.java] - _binding = FragmentTransactionBinding.inflate(inflater, container, false) + transactionViewModel.listTransactions.observe(viewLifecycleOwner) { transactionList -> + transactionAdapter.setTransactionList(transactionList) + } + } + + private fun setupRecyclerView() { + transactionAdapter = TransactionAdapter(transactionViewModel) + binding.transactionList.apply { + layoutManager = LinearLayoutManager(context) + adapter = transactionAdapter + } + } - binding.button.setOnClickListener { + private fun setupClickListeners() { + binding.addTransactionButton.setOnClickListener { val intent = Intent(activity, AddTransactionActivity::class.java) startActivity(intent) } - - return binding.root } override fun onDestroyView() { 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 cf96283f3289be47bfd3205f7e6faa0ac4467748..e34a11eb51d9dcf434b68e8089586dab0911a7a2 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 @@ -21,4 +21,8 @@ class TransactionViewModel(private val repository : TransactionRepository) : Vie fun deleteTransaction(transaction: TransactionEntity) = viewModelScope.launch { repository.deleteTransaction(transaction) } + + fun getTransactionById(id: Int): LiveData<TransactionEntity> { + return repository.getTransactionById(id) + } } \ 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/java/com/onionsquad/bondoman/util/DateConverter.kt b/app/src/main/java/com/onionsquad/bondoman/util/DateConverter.kt deleted file mode 100644 index 408d43f29d0fef1feb655b043a8080594af926e5..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/onionsquad/bondoman/util/DateConverter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.onionsquad.bondoman.util - -import androidx.room.TypeConverter -import java.util.Date - -object DateConverter { - - @TypeConverter - @JvmStatic - fun fromTimestamp(value: Long?): Date? { - return value?.let { Date(it) } - } - - @TypeConverter - @JvmStatic - fun dateToTimestamp(value: Date?): Long? { - return value?.time - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/card_background.xml b/app/src/main/res/drawable/card_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..9687bcb49124a23fdc528e7bcfc09da50ff742f0 --- /dev/null +++ b/app/src/main/res/drawable/card_background.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#FFBB86FC" /> + <corners android:radius="8dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000000000000000000000000000000000000..8140012280ccf471ae3e11e44058ebfb0a4fa6e4 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> +</vector> \ 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_logout_24.xml b/app/src/main/res/drawable/ic_baseline_logout_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..c22a96f911c6a8c7615c8e149ee796eddc23d449 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_logout_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="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/> + +</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/drawable/ic_baseline_signal_wifi_connected_no_internet_4_24.xml b/app/src/main/res/drawable/ic_baseline_signal_wifi_connected_no_internet_4_24.xml new file mode 100644 index 0000000000000000000000000000000000000000..49da62c2a8894234fe34497c062850789200b223 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_signal_wifi_connected_no_internet_4_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="M24,8.98C20.93,5.9 16.69,4 12,4C7.31,4 3.07,5.9 0,8.98L12,21v-9h8.99L24,8.98zM19.59,14l-2.09,2.09L15.41,14L14,15.41l2.09,2.09L14,19.59L15.41,21l2.09,-2.08L19.59,21L21,19.59l-2.08,-2.09L21,15.41L19.59,14z"/> + +</vector> diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000000000000000000000000000000000000..618a75853cf62efae9734d66d4e3cdd2396fd080 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <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> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000000000000000000000000000000000000..3ea930d89592aa614ceb09cefe6ee449dc55f1da --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <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> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_add_transaction.xml b/app/src/main/res/layout/activity_add_transaction.xml index 1858a85b38c01dfd8eda38a6101a69ec026fd3a5..95fa2b3af87c278d7b251600cecf1c1b0d0ddacb 100644 --- a/app/src/main/res/layout/activity_add_transaction.xml +++ b/app/src/main/res/layout/activity_add_transaction.xml @@ -28,13 +28,26 @@ android:hint="Nominal" android:inputType="numberDecimal" /> - <EditText - android:id="@+id/categoryEditText" + <RadioGroup + android:id="@+id/categoryRadioGroup" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:hint="Kategori" - android:inputType="text" /> + 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" diff --git a/app/src/main/res/layout/activity_edit_transaction.xml b/app/src/main/res/layout/activity_edit_transaction.xml new file mode 100644 index 0000000000000000000000000000000000000000..8b23b5075a31acd86cc49cdd54a73576f5b219d7 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_transaction.xml @@ -0,0 +1,66 @@ +<?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="Edit 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/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000000000000000000000000000000000000..934a2edc6b2b4eacc44b02b986b6628f51873712 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> + + <EditText + android:id="@+id/username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="96dp" + android:autofillHints="@string/prompt_email" + android:hint="@string/prompt_email" + android:inputType="textEmailAddress" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <EditText + android:id="@+id/password" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:autofillHints="@string/prompt_password" + android:hint="@string/prompt_password" + android:imeActionLabel="@string/action_sign_in" + android:imeOptions="actionDone" + android:inputType="textPassword" + android:selectAllOnFocus="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/username" /> + + <Button + android:id="@+id/login" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_marginTop="16dp" + android:layout_marginBottom="64dp" + android:enabled="false" + android:text="@string/action_sign_in" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/password" + app:layout_constraintVertical_bias="0.2" /> + + <ProgressBar + android:id="@+id/loading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="64dp" + android:layout_marginBottom="64dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/password" + app:layout_constraintStart_toStartOf="@+id/password" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.3" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_no_internet.xml b/app/src/main/res/layout/activity_no_internet.xml new file mode 100644 index 0000000000000000000000000000000000000000..83fadbcdb8ddb17ee2e949e3f0c4259e159de028 --- /dev/null +++ b/app/src/main/res/layout/activity_no_internet.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout 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"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + tools:layout_editor_absoluteX="1dp" + tools:layout_editor_absoluteY="1dp" > + + <ImageView + android:id="@+id/no_internet_image" + android:layout_width="match_parent" + android:layout_height="160dp" + android:alpha="0.2" + android:contentDescription="@string/no_internet" + app:srcCompat="@drawable/ic_baseline_signal_wifi_connected_no_internet_4_24" /> + + <TextView + android:id="@+id/text_no_internet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="30sp" + android:textAlignment="center" + android:alpha="0.2" + android:text="@string/no_internet" /> + </LinearLayout> +</RelativeLayout> \ 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 441476f744f7bedaef0590d7b4b632a82ee0354d..b4fa8f9139a6487708df28d49918d4d29402779e 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,8 +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"> -</FrameLayout> \ No newline at end of file + <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/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/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" + 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 26af3dd3316b3d39d8d4a93547724bcb95a87311..f6bdd20ec3dd92fcc5f68be9c57f83d2e5c47bb5 100644 --- a/app/src/main/res/layout/fragment_transaction.xml +++ b/app/src/main/res/layout/fragment_transaction.xml @@ -6,15 +6,25 @@ android:layout_height="match_parent" tools:context=".ui.transaction.TransactionFragment"> - <Button - android:id="@+id/button" + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/transactionList" + android:layout_width="0dp" + android:layout_height="0dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:listitem="@layout/transaction_card" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/addTransactionButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Button" - android:visibility="visible" + android:layout_margin="16dp" + android:src="@drawable/ic_add" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:backgroundTint="#FF6200EE" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/transaction_card.xml b/app/src/main/res/layout/transaction_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..0460ea905c63c1d90b81d1d5b48e56429e263623 --- /dev/null +++ b/app/src/main/res/layout/transaction_card.xml @@ -0,0 +1,96 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="10dp" + app:cardCornerRadius="8dp" + app:cardElevation="4dp"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/card_background" + android:padding="16dp"> + + <TextView + android:id="@+id/dateTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:textStyle="bold" + android:textColor="#FFFFFF" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="29/02/2024" /> + + <TextView + android:id="@+id/categoryTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:textSize="14sp" + android:textColor="#FFFFFF" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dateTextView" + tools:text="Pembelian" /> + + <TextView + android:id="@+id/titleTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="18sp" + android:textStyle="bold" + android:textColor="#FFFFFF" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/categoryTextView" + tools:text="Transaction Name" /> + + <TextView + android:id="@+id/amountTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:textColor="#FFFFFF" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="IDR 15.000" /> + + <TextView + android:id="@+id/locationTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="14sp" + android:textColor="#FFFFFF" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/amountTextView" + tools:text="Location" /> + + <ImageButton + android:id="@+id/editButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="10dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:src="@drawable/ic_edit" + app:layout_constraintEnd_toStartOf="@+id/deleteButton" + app:layout_constraintTop_toBottomOf="@id/titleTextView" + app:tint="#FFFFFF" /> + + <ImageButton + android:id="@+id/deleteButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:src="@drawable/ic_delete" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/titleTextView" + app:tint="#FFFFFF" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.cardview.widget.CardView> \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 56c03a8376f28c9b6734087a3ff220d5f679beeb..e583f294812112bb767d715ad02a0e68165ac49a 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -13,4 +13,9 @@ <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> </style> + <!-- Base application theme. --> + <style name="Base.Theme.Bondoman" parent="Theme.Material3.DayNight.NoActionBar"> + <!-- Customize your dark theme here. --> + <!-- <item name="colorPrimary">@color/my_dark_primary</item> --> + </style> </resources> \ 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/dimens.xml b/app/src/main/res/values/dimens.xml index f49468903d6e80e80d57478229c158ae05fd7b11..5c9f62875de7081ca2d6d39939abc9c1eee84cba 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,4 +3,5 @@ <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="graph_no_data_text">20sp</dimen> + <dimen name="fab_margin">16dp</dimen> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d737aefdf31b4dc1aebb0a6abcc7082e2efa1e9..41677d694a5d90740d494a11f048bb3cecb681ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,66 @@ <string name="title_graph">Graph</string> <string name="title_settings">Settings</string> <string name="graph_no_data">No data</string> + <string name="title_activity_login">Sign in</string> + <string name="prompt_email">Email</string> + <string name="prompt_password">Password</string> + <string name="action_sign_in">Sign in</string> + <string name="login_success">"Login success!"</string> + <string name="invalid_email">Not a valid email</string> + <string name="login_failed">"Login failed"</string> + <string name="preference_file_key">BondomanSharedPrefs</string> + <string name="action_sign_out">Sign out</string> + <string name="title_alert_logout">Confirm sign out</string> + <string name="message_alert_logout">Are you sure you want to sign out?</string> + <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> + <string name="internet_not_connected">You are not connecting to internet</string> + <!-- Strings used for fragments for navigation --> + <string name="first_fragment_label">First Fragment</string> + <string name="second_fragment_label">Second Fragment</string> + <string name="next">Next</string> + <string name="previous">Previous</string> + + <string name="lorem_ipsum"> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris + volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus + dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad + litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend + diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a, + ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n + Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus + egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed + neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada + fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae, + molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor + bibendum, vel congue leo egestas.\n\n + Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit + amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel, + molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer + interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at + lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula, + in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque + est.\n\n + Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh. + Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui + non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In + eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc, + quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra + ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a + placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus + convallis.\n\n + Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et + malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa + gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper, + libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper + sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus + libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus + vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. + </string> + <string name="no_internet">No internet</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c39849b812b4a960243b412e67ae6101d43e244f..6d562401242a5cbb0c942e57f091934d7c7f4796 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -13,4 +13,9 @@ <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <!-- Customize your theme here. --> </style> + <!-- Base application theme. --> + <style name="Base.Theme.Bondoman" parent="Theme.Material3.DayNight.NoActionBar"> + <!-- Customize your light theme here. --> + <!-- <item name="colorPrimary">@color/my_light_primary</item> --> + </style> </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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10345b9864bd86655501aca6c855f98b780b82f1..214babfbd91ed3d7211cbe30f42207a6fbea57ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,11 @@ room = "2.6.1" kapt = "1.9.23" lifecycleViewmodelCompose = "2.7.0" chart = "v3.1.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" } @@ -34,7 +39,17 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = 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" } +<<<<<<< gradle/libs.versions.toml mpandroidchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "chart" } +======= +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" } +>>>>>>> gradle/libs.versions.toml [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }