diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2df39f1240544c6d3460e77341b980cf2048d9c4..0089e4fbb12a612f9ec58933fead21efc8ec8fdd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") } android { @@ -33,22 +34,59 @@ android { buildFeatures { viewBinding = true } + + packaging { + resources.excludes.add("META-INF/DEPENDENCIES") + } } + + dependencies { + implementation("com.google.android.gms:play-services-location:21.2.0") + val roomVersion = "2.6.1" implementation("androidx.core:core-ktx:1.8.0") implementation("androidx.appcompat:appcompat:1.5.1") implementation("com.google.android.material:material:1.9.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") implementation("androidx.navigation:navigation-ui-ktx:2.5.3") + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.fragment:fragment-ktx:1.5.7") implementation("androidx.annotation:annotation:1.6.0") + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation("com.squareup.moshi:moshi:1.15.0") + implementation("com.squareup.moshi:moshi-kotlin:1.15.0") + implementation("com.squareup.retrofit2:converter-moshi:2.9.0") + implementation("androidx.preference:preference:1.2.1") + + implementation("io.github.evanrupert:excelkt:1.0.2") + + implementation("androidx.room:room-ktx:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("androidx.camera:camera-camera2:1.3.2") + implementation("androidx.camera:camera-view:1.3.2") + implementation("androidx.camera:camera-lifecycle:1.3.2") + + implementation("androidx.activity:activity-ktx:1.8.2") + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e55f0a7a984a0cbf707958ae60b2b92f7793b918..55fee1665cff2f3ad7c1128bbb5fe531a776a3a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,24 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> - - <uses-permission android:name="android.permission.INTERNET"/> - <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> + + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <application + android:name=".ui.transactions.TransactionsApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" @@ -15,16 +29,47 @@ android:theme="@style/Theme.BondoYap" tools:targetApi="31"> + <service + android:name=".service.jwt.JwtService" + android:enabled="true" + android:exported="true" + android:permission="android.permission.INTERNET"></service> + + <receiver + android:name=".ui.transactions.TransactionsBroadcastReceiver" + android:enabled="true" + android:exported="false"> + <intent-filter> + <action android:name="com.BondoYap.transactions.randomize" /> + </intent-filter> + </receiver> + <activity - android:name=".MainActivity" + android:name=".ui.login.LoginActivity" android:exported="true" - android:label="@string/app_name"> + android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + + <activity + android:name=".MainActivity" + android:exported="true"></activity> + </application> + <queries> + <intent> + <action android:name="android.intent.action.VIEW" /> + <data android:scheme="geo" /> + </intent> + <intent> + <action android:name="android.intent.action.VIEW" /> + <data android:scheme="https" /> + </intent> + </queries> + </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/MainActivity.kt b/app/src/main/java/com/example/bondoyap/MainActivity.kt index 2e8d94000a9261abe7a5da083f5c0373dda0efb5..f02eacef559d1993ef2fcfc82c897ae99dd3fb41 100644 --- a/app/src/main/java/com/example/bondoyap/MainActivity.kt +++ b/app/src/main/java/com/example/bondoyap/MainActivity.kt @@ -1,29 +1,46 @@ package com.example.bondoyap import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.SharedPreferences import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.example.bondoyap.databinding.ActivityMainBinding +import com.example.bondoyap.service.api.Constants +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.service.jwt.JwtService +import com.example.bondoyap.ui.login.LoginActivity +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsBroadcastReceiver +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory import com.example.bondoyap.util.network.NetworkObserver import com.google.android.material.bottomnavigation.BottomNavigationView - class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var sharedPreferences: SharedPreferences private lateinit var networkObserver: NetworkObserver + private lateinit var transactionsBroadcastReceiver: TransactionsBroadcastReceiver + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((applicationContext as TransactionsApplication).repository) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + networkObserver = NetworkObserver(applicationContext) networkObserver.isConnected.observe(this) { isConnected -> if (isConnected) { @@ -33,6 +50,15 @@ class MainActivity : AppCompatActivity() { } } + if (!isLoggedIn()) { + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + } + + val serviceIntent = Intent(this, JwtService::class.java) + this.startService(serviceIntent) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -41,16 +67,10 @@ class MainActivity : AppCompatActivity() { val navController = findNavController(R.id.nav_host_fragment_activity_main) - sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE) - - if (!isLoggedIn()) { - navController.navigate(R.id.navigation_login) - } - val appBarConfiguration = AppBarConfiguration( setOf( R.id.navigation_settings, R.id.navigation_transactions, R.id.navigation_scanner, - R.id.navigation_graph, R.id.navigation_login + R.id.navigation_graph ) ) @@ -58,18 +78,23 @@ class MainActivity : AppCompatActivity() { navView.setupWithNavController(navController) - navController.addOnDestinationChangedListener { _, destination, _ -> - // Disable bottom navigation when on the login screen - if (destination.id == R.id.navigation_login) { - navView.visibility = BottomNavigationView.GONE - } else { - navView.visibility = BottomNavigationView.VISIBLE - } - } + transactionsBroadcastReceiver = TransactionsBroadcastReceiver(transactionsViewModel) + val filter = IntentFilter(Constants.ACTION_RANDOMIZE_TRANSACTIONS) + LocalBroadcastManager.getInstance(this) + .registerReceiver(transactionsBroadcastReceiver, filter) + } + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_activity_main) + return navController.navigateUp() || super.onSupportNavigateUp() } private fun isLoggedIn(): Boolean { return sharedPreferences.getBoolean("isLoggedIn", false) } + + override fun onDestroy() { + super.onDestroy() + LocalBroadcastManager.getInstance(this).unregisterReceiver(transactionsBroadcastReceiver) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/LocationManager.kt b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..3196f9aa9dbadf66b1e00ea7082b75c1a8e45cbf --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt @@ -0,0 +1,61 @@ +package com.example.bondoyap.service + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.fragment.app.FragmentActivity +import com.google.android.gms.location.LocationServices + +class LocationManager { + companion object { + fun haveLocationPermission(context: Context): Boolean { + return (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED) + } + + fun askLocationPermission(context: Context, activity: FragmentActivity) { + if (!haveLocationPermission(context)) { + ActivityCompat.requestPermissions( + activity, arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + } + } + + @SuppressLint("MissingPermission") + fun getLocation(context: Context): Location? { + var location: Location? = null + if (haveLocationPermission(context)) { + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(context) + Log.d("LocationManager", "Getting last location") + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + location = it + } + } + } + + if (location == null) { + Log.d("LocationManager", "Location is null") + } else { + Log.d( + "LocationManager", + "Get location at latitude: ${location?.latitude} and longitude: ${location?.longitude}" + ) + } + return location + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/SessionManager.kt b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a58fbedd688c66addcce7c0d7e46097fd22abd4 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/SessionManager.kt @@ -0,0 +1,52 @@ +package com.example.bondoyap.service + +import android.content.Context +import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.ui.login.data.model.LoggedInUser +import com.squareup.moshi.Moshi +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +class SessionManager(context: Context) { + + private var prefs: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, + Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = prefs.edit() + + + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) + + private fun getUser(): LoggedInUser? { + val userJson = prefs.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + + fun saveExp(exp: Long){ + editor.putLong("exp", exp) + editor.apply() + } + + fun hasExp(): Boolean{ + return getExp().toInt() != -1 + } + + fun getExp(): Long { + return prefs.getLong("exp", -1) + } + + fun getToken(): String? { + val user = getUser() + return user?.let { + user.token + } + } + + fun logout(){ + editor.clear() + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..02b03eedad67243043d2ec12012358a46c6ca360 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiClient.kt @@ -0,0 +1,40 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory + +class ApiClient { + private lateinit var apiService: ApiService + fun getApiService(context: Context): ApiService { + + if (!::apiService.isInitialized) { + + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + val retrofit = Retrofit.Builder() + .baseUrl(Constants.BASE_URL) + .client(okhttpClient(context)) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + + apiService = retrofit.create(ApiService::class.java) + } + return apiService + } + + private fun okhttpClient(context: Context): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(AuthInterceptor(context)) + .build() + } + +} + + + diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt new file mode 100644 index 0000000000000000000000000000000000000000..803015586a080eda094a614661e240d63a0eebe1 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt @@ -0,0 +1,30 @@ +package com.example.bondoyap.service.api + +import com.example.bondoyap.service.api.data.BillResponse +import com.example.bondoyap.service.api.data.LoginRequest +import com.example.bondoyap.service.api.data.LoginResponse +import com.example.bondoyap.service.api.data.TokenResponse +import okhttp3.MultipartBody +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface ApiService { + @POST("auth/login") + fun login( + @Body loginRequest: LoginRequest + ): Call<LoginResponse> + + @POST("auth/token") + fun verifyToken( + @Body token: String + ): Call<TokenResponse> + + @POST("bill/upload") + @Multipart + fun getBill( + @Part photoPart : MultipartBody.Part + ) : Call<BillResponse> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt new file mode 100644 index 0000000000000000000000000000000000000000..62f933c2fb87585dbdccb4095fd668ea970d4d1c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/AuthInterceptor.kt @@ -0,0 +1,21 @@ +package com.example.bondoyap.service.api + +import android.content.Context +import com.example.bondoyap.service.SessionManager +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor(context: Context) : Interceptor { + + private val sessionManager = SessionManager(context) + + override fun intercept(chain: Interceptor.Chain): Response { + val requestBuilder = chain.request().newBuilder() + + sessionManager.getToken()?.let { + requestBuilder.addHeader("Authorization", "Bearer $it") + } + + return chain.proceed(requestBuilder.build()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/Constants.kt b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt new file mode 100644 index 0000000000000000000000000000000000000000..897daedea631dae9a33ff6c0eca3671394eb39df --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/Constants.kt @@ -0,0 +1,7 @@ +package com.example.bondoyap.service.api + +object Constants { + const val BASE_URL: String = "https://pbd-backend-2024.vercel.app/api/" + const val SHARED_PREFS_NAME = "BondoYap" + const val ACTION_RANDOMIZE_TRANSACTIONS = "com.BondoYap.transactions.randomize" +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..098c5e658bc2c42e7acb89deea7e3ff7514ea188 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class BillResponse( + val items : Items +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd01390409cf0b82f1638f536e0d814e988817be --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt @@ -0,0 +1,9 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Item( + val name : String, + @Json(name = "qty") + val quantity : Int, + val price : Double +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c779b17b1baebb2337f106ad94a51398ea47458 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Items( + val items : List<Item> +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..583c050d0c129816df5e0901d18bdc0a60534c14 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginRequest.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data + +data class LoginRequest( + val email: String, + val password: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca1bb881dc6e0eabc38cb64374fd2eee615d6a52 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/LoginResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class LoginResponse( + val token: String +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f8a2be82dbdbfd2846355c965f5072680a02efb --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/TokenResponse.kt @@ -0,0 +1,7 @@ +package com.example.bondoyap.service.api.data + +data class TokenResponse( + val nim: String, + val iat: Long, + val exp: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt new file mode 100644 index 0000000000000000000000000000000000000000..39bfd27dd4a6dcd60679e300b6ed963d2e34071f --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/jwt/JwtService.kt @@ -0,0 +1,115 @@ +package com.example.bondoyap.service.jwt + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.IBinder +import android.util.Log +import android.widget.Toast +import com.example.bondoyap.service.SessionManager +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.TokenResponse +import com.example.bondoyap.ui.login.LoginActivity +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + + +class JwtService : Service() { + + private lateinit var handler : Handler + + private lateinit var session: SessionManager + + private val task = object : Runnable { + override fun run() { + Log.d("JwtService", "Current Time: ${System.currentTimeMillis()}") + + session = SessionManager(applicationContext) + val token = session.getToken() + + if (token != null) { + verifyJwt(applicationContext, token) + } + // 10 second + handler.postDelayed(this, 10000) + } + } + + private lateinit var apiClient: ApiClient + + override fun onCreate() { + super.onCreate() + handler = Handler(mainLooper) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + handler.post(task) + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + handler.removeCallbacks(task) + } + + fun verifyJwt(context: Context, token: String){ + if (session.hasExp()) { + checkJwt(session.getExp()) + return + } + apiClient = ApiClient() + apiClient.getApiService(context).verifyToken(token).enqueue(object: Callback<TokenResponse>{ + override fun onFailure(call: Call<TokenResponse>, t: Throwable) { + Log.d("JwtService", "Error Failure") + } + + override fun onResponse(call: Call<TokenResponse>, response: Response<TokenResponse>) { + Log.d("JwtService", "Getting Response") + + val tokenResponse = response.body() + + if (tokenResponse != null){ + Log.d("JwtService", "Updating Exp") + + session.saveExp(tokenResponse.exp) + checkJwt(tokenResponse.exp) + } else { + Log.d("JwtService", "Error response") + Log.d("JwtService", "Error expired?") + handleExpired() + } + + } + }) + } + + fun checkJwt(exp: Long) { + val currentTimestamp = System.currentTimeMillis() / 1000 + + Log.d("JwtService", "exp : ${exp}") + Log.d("JwtService", "currentTime : ${currentTimestamp}") + + if (currentTimestamp >= exp) { + Log.d("JwtService", "JWT is expired") + handleExpired() + } else { + Log.d("JwtService", "JWT is still valid") + } + } + + fun handleExpired(){ + session.logout() + Toast.makeText(applicationContext, "Session Expired! \n Logging out ...", Toast.LENGTH_LONG).show() + + val intent = Intent(applicationContext, LoginActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startActivity(intent) + stopSelf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt index 127052153ac667ee17007bf12a87861dfd93308f..106a5999d4bc31f2babf13d5b526b17ff648626e 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoggedInUserView.kt @@ -4,6 +4,6 @@ package com.example.bondoyap.ui.login * User details post authentication that is exposed to the UI */ data class LoggedInUserView( - val displayName: String + val email: String //... other data fields that may be accessible to the UI ) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..e21b23e4fa0c8c171cb0f1e56f67f635e6dae6b2 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginActivity.kt @@ -0,0 +1,165 @@ +package com.example.bondoyap.ui.login + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.annotation.StringRes +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.example.bondoyap.MainActivity +import com.example.bondoyap.R +import com.example.bondoyap.databinding.ActivityLoginBinding + +class LoginActivity : AppCompatActivity() { + + private lateinit var loginViewModel: LoginViewModel + private lateinit var binding: ActivityLoginBinding + private lateinit var sharedPreferences: SharedPreferences + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + + + if (isLoggedIn()) { + navigateToMainActivity() + finish() + } else { + binding = ActivityLoginBinding.inflate(layoutInflater) + setContentView(binding.root) + + val emailEditText = binding.email + val passwordEditText = binding.password + val loginButton = binding.login + val loadingProgressBar = binding.loading + + val factory = LoginViewModelFactory(this) + loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java] + + // skip login + loginViewModel.login( + "13521xxx@std.stei.itb.ac.id", + "password_13521xxx" + ) + if (isLoggedIn()) { + navigateToMainActivity() + finish() + } + + loginViewModel.loginFormState.observe(this@LoginActivity, Observer { + val loginState = it ?: return@Observer + + // disable login button unless both username / password is valid + loginButton.isEnabled = loginState.isDataValid + + if (loginState.emailError != null) { + emailEditText.error = getString(loginState.emailError) + } + if (loginState.passwordError != null) { + passwordEditText.error = getString(loginState.passwordError) + } + }) + + loginViewModel.loginResult.observe(this@LoginActivity, Observer { + val loginResult = it ?: return@Observer + + loadingProgressBar.visibility = View.GONE + if (loginResult.error != null) { + showLoginFailed(loginResult.error) + } + if (loginResult.success != null) { + updateUiWithUser(loginResult.success) + setResult(RESULT_OK) + finish() + } + }) + + emailEditText.afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + passwordEditText.apply { + afterTextChanged { + loginViewModel.loginDataChanged( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + + setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + false + } + + loginButton.setOnClickListener { + loadingProgressBar.visibility = View.VISIBLE + loginViewModel.login( + emailEditText.text.toString(), + passwordEditText.text.toString() + ) + } + } + } + } + + private fun updateUiWithUser(model: LoggedInUserView) { + val welcome = getString(R.string.welcome) + "\n" + model.email + // TODO : initiate successful logged in experience + Toast.makeText( + applicationContext, + welcome, + Toast.LENGTH_SHORT + ).show() + navigateToMainActivity() + } + + private fun showLoginFailed(@StringRes errorString: Int) { + Toast.makeText(applicationContext, errorString, Toast.LENGTH_SHORT).show() + } + + private fun navigateToMainActivity() { + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + finish() + } + + private fun isLoggedIn(): Boolean { + return sharedPreferences.getBoolean("isLoggedIn", false) + } +} + +/** + * Extension function to simplify setting an afterTextChanged action to EditText components. + */ +fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { + this.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(editable: Editable?) { + afterTextChanged.invoke(editable.toString()) + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginFragment.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginFragment.kt deleted file mode 100644 index 2fa73d7b619d82ec9bcd99dd44731ea250e8830a..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.bondoyap.ui.login - -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import androidx.annotation.StringRes -import androidx.fragment.app.Fragment -import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.Toast -import androidx.navigation.fragment.findNavController -import com.example.bondoyap.R -import com.example.bondoyap.databinding.FragmentLoginBinding - -class LoginFragment : Fragment() { - - private lateinit var loginViewModel: LoginViewModel - private var _binding: FragmentLoginBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - - _binding = FragmentLoginBinding.inflate(inflater, container, false) - return binding.root - - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - loginViewModel = ViewModelProvider(this, LoginViewModelFactory()) - .get(LoginViewModel::class.java) - - val emailEditText = binding.email - val passwordEditText = binding.password - val loginButton = binding.login - val loadingProgressBar = binding.loading - - loginViewModel.loginFormState.observe(viewLifecycleOwner, - Observer { loginFormState -> - if (loginFormState == null) { - return@Observer - } - loginButton.isEnabled = loginFormState.isDataValid - loginFormState.emailError?.let { - emailEditText.error = getString(it) - } - loginFormState.passwordError?.let { - passwordEditText.error = getString(it) - } - }) - - loginViewModel.loginResult.observe(viewLifecycleOwner, - Observer { loginResult -> - loginResult ?: return@Observer - loadingProgressBar.visibility = View.GONE - loginResult.error?.let { - showLoginFailed(it) - } - loginResult.success?.let { - updateUiWithUser(it) - requireActivity().actionBar?.setDisplayHomeAsUpEnabled(false) - navigateToTransactionFragment() - } - }) - - val afterTextChangedListener = object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - // ignore - } - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - // ignore - } - - override fun afterTextChanged(s: Editable) { - loginViewModel.loginDataChanged( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - } - emailEditText.addTextChangedListener(afterTextChangedListener) - passwordEditText.addTextChangedListener(afterTextChangedListener) - passwordEditText.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - loginViewModel.login( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - false - } - - loginButton.setOnClickListener { - loadingProgressBar.visibility = View.VISIBLE - loginViewModel.login( - emailEditText.text.toString(), - passwordEditText.text.toString() - ) - } - } - - private fun updateUiWithUser(model: LoggedInUserView) { - val welcome = getString(R.string.welcome) + model.displayName - // TODO : initiate successful logged in experience - val appContext = context?.applicationContext ?: return - Toast.makeText(appContext, welcome, Toast.LENGTH_LONG).show() - } - - private fun showLoginFailed(@StringRes errorString: Int) { - val appContext = context?.applicationContext ?: return - Toast.makeText(appContext, errorString, Toast.LENGTH_LONG).show() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun navigateToTransactionFragment() { - findNavController().navigate(R.id.navigation_transactions) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt index 11feb87bc1487a0f9624658311d6fe0bfc11b1c6..fedbf7d9cd2c6686bf8dc47cdf25d6cf84422a40 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModel.kt @@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import android.util.Patterns +import androidx.lifecycle.viewModelScope import com.example.bondoyap.R import com.example.bondoyap.ui.login.data.LoginRepository import com.example.bondoyap.ui.login.data.Result +import kotlinx.coroutines.launch class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() { @@ -17,13 +19,14 @@ class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() val loginResult: LiveData<LoginResult> = _loginResult fun login(email: String, password: String) { - val result = loginRepository.login(email, password) - - if (result is Result.Success) { - _loginResult.value = - LoginResult(success = LoggedInUserView(displayName = result.data.displayName)) - } else { - _loginResult.value = LoginResult(error = R.string.login_failed) + viewModelScope.launch { + val result = loginRepository.login(email, password) + if (result is Result.Success) { + _loginResult.value = + LoginResult(success = LoggedInUserView(email = result.data.email)) + } else { + _loginResult.value = LoginResult(error = R.string.login_failed) + } } } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt index de741ade55806d4f3612cf2ef705e3d13874dc9d..0b7abfa109b689c808959179ec0729362d3c2a8d 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/LoginViewModelFactory.kt @@ -1,22 +1,21 @@ package com.example.bondoyap.ui.login +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.bondoyap.ui.login.data.LoginDataSource import com.example.bondoyap.ui.login.data.LoginRepository -/** - * ViewModel provider factory to instantiate LoginViewModel. - * Required given LoginViewModel has a non-empty constructor - */ -class LoginViewModelFactory : ViewModelProvider.Factory { +class LoginViewModelFactory(private val context: Context) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { return LoginViewModel( loginRepository = LoginRepository( - dataSource = LoginDataSource() + dataSource = LoginDataSource(context), + context = context + ) ) as T } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt index 3e221c0a19f9dc26f316fee64c9cfae5734e84f1..00f4b562726b80158e1de36da69e93f895b347ed 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginDataSource.kt @@ -1,24 +1,58 @@ package com.example.bondoyap.ui.login.data +import android.content.Context +import android.util.Log +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.LoginRequest import com.example.bondoyap.ui.login.data.model.LoggedInUser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.IOException /** * Class that handles authentication w/ login credentials and retrieves user information. */ -class LoginDataSource { +class LoginDataSource(private val context: Context) { - fun login(email: String, password: String): Result<LoggedInUser> { + private lateinit var apiClient: ApiClient + + suspend fun login(email: String, password: String): Result<LoggedInUser> { return try { - // TODO: handle loggedInUser authentication - val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") - Result.Success(fakeUser) + + val token = postLogin(email, password) + + val parts = email.split("@") + val nim = parts[0] + + val user = LoggedInUser(nim, email, token) + + Result.Success(user) } catch (e: Throwable) { Result.Error(IOException("Error logging in", e)) } } - fun logout() { - // TODO: revoke authentication + private suspend fun postLogin(email: String, password: String):String { + + apiClient = ApiClient() + val service = apiClient.getApiService(context) + + return withContext(Dispatchers.IO) { + try { + val loginRequest = LoginRequest(email, password) + val response = service.login(loginRequest).execute() + + if (response.isSuccessful) { + val loginResponse = response.body() + loginResponse?.token ?: throw IOException("Token not received") + } else { + Log.d("LoginDataSource", "${response.code()} ${response.message()}") + throw IOException("${response.code()} ${response.message()}") + } + } catch (e: Exception) { + Log.d("LoginDataSource", "Login failed: ${e.message}") + throw IOException("Login failed: ${e.message}") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt index 840ffe51ce4b5e3a307ad9c7c0442c11d14642ee..1d7293786899ffa28dc67352cbcd15d0149e34ab 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/LoginRepository.kt @@ -1,34 +1,30 @@ package com.example.bondoyap.ui.login.data import com.example.bondoyap.ui.login.data.model.LoggedInUser +import android.content.Context +import android.content.SharedPreferences +import com.example.bondoyap.service.api.Constants.SHARED_PREFS_NAME +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory /** * Class that requests authentication and user information from the remote data source and * maintains an in-memory cache of login status and user credentials information. */ -class LoginRepository(val dataSource: LoginDataSource) { +class LoginRepository(val dataSource: LoginDataSource, context: Context) { - // in-memory cache of the loggedInUser object - var user: LoggedInUser? = null - private set + private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + private val editor: SharedPreferences.Editor = sharedPreferences.edit() - val isLoggedIn: Boolean - get() = user != null - - init { - // If user credentials will be cached in local storage, it is recommended it be encrypted - // @see https://developer.android.com/training/articles/keystore - user = null - } + private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + private val userAdapter: JsonAdapter<LoggedInUser> = moshi.adapter(LoggedInUser::class.java) - fun logout() { - user = null - dataSource.logout() - } + val isLoggedIn: Boolean + get() = sharedPreferences.getBoolean("isLoggedIn", false) - fun login(email: String, password: String): Result<LoggedInUser> { - // handle login + suspend fun login(email: String, password: String): Result<LoggedInUser> { val result = dataSource.login(email, password) if (result is Result.Success) { @@ -38,8 +34,22 @@ class LoginRepository(val dataSource: LoginDataSource) { return result } + fun logout(){ + editor.clear() + editor.apply() + } + + fun getUser(): LoggedInUser? { + val userJson = sharedPreferences.getString("loggedInUser", null) + return userJson?.let { + userAdapter.fromJson(it) + } + } + private fun setLoggedInUser(loggedInUser: LoggedInUser) { - this.user = loggedInUser + val userJson = userAdapter.toJson(loggedInUser) + editor.putString("loggedInUser", userJson).apply() + editor.putBoolean("isLoggedIn", true).apply() // If user credentials will be cached in local storage, it is recommended it be encrypted // @see https://developer.android.com/training/articles/keystore } diff --git a/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt index 10a9aa93f64d280b3861bd282307ab0213e011df..78d7e033176d0673dfb6016ebacf1d2c54c256e0 100644 --- a/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt +++ b/app/src/main/java/com/example/bondoyap/ui/login/data/model/LoggedInUser.kt @@ -5,5 +5,6 @@ package com.example.bondoyap.ui.login.data.model */ data class LoggedInUser( val userId: String, - val displayName: String + val email: String, + val token: String ) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c7913390e762e7536716cb0e985dda5702e0357 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt @@ -0,0 +1,139 @@ +package com.example.bondoyap.ui.scanner + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.bondoyap.databinding.FragmentScanResultBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.service.api.data.Item +import com.example.bondoyap.ui.scanner.listAdapter.ScanResultListAdapter +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +class ScanResultFragment : Fragment() { + // This property is only valid between onCreateView and + // onDestroyView. + private var _binding: FragmentScanResultBinding? = null + private val binding get() = _binding!! + private lateinit var navController: NavController + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentScanResultBinding.inflate(inflater, container, false) + val root: View = binding.root + + navController = findNavController() + val scannerViewModel: ScannerViewModel by activityViewModels() + val items = scannerViewModel.items ?: throw Exception("Scan Result is null") + Log.d("ScanResult", "Result Received: $items") + + val recyclerView = binding.recyclerView + val adapter = ScanResultListAdapter() + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + adapter.submitList(items) + + binding.cancelButton.setOnClickListener { + navController.popBackStack() + } + + binding.saveButton.setOnClickListener { + saveNota(items) + } + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun saveNota(items: List<Item>) { + try { + Log.d("ScanResult", "Saving note: $items") + saveNotaRepository(items) + Toast.makeText(requireContext(), "Nota berhasil disimpan", Toast.LENGTH_SHORT).show() + navController.popBackStack() + } catch (e: Exception) { + Toast.makeText(requireContext(), "Nota gagal disimpan", Toast.LENGTH_SHORT).show() + Log.e("ScanResult", "Saving note failed:", e) + } + } + + private fun saveNotaRepository(items: List<Item>) { + val dateDateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val titleDateFormat = SimpleDateFormat("dd_MM_yyyy_HH_mm_ss", Locale.getDefault()) + val currentTime = Date() + val currentDate = dateDateFormat.format(currentTime) + val title = titleDateFormat.format(currentTime) + + val value = items.sumOf { it.quantity * it.price } + + val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + Log.d("ScanResult", "Getting location") + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("ScanResult", "Saving note on database") + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } + } + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt index 0996dc927745fd359a7ac320a90b845d8ffb9083..94353b0bae865f894553b87498625e90e009a073 100644 --- a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt @@ -1,37 +1,144 @@ package com.example.bondoyap.ui.scanner +import android.Manifest +import android.app.Activity +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.provider.MediaStore +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.R import com.example.bondoyap.databinding.FragmentScannerBinding +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.BillResponse +import com.example.bondoyap.service.api.data.Items +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.FileOutputStream -class ScannerFragment : Fragment() { - - private var _binding: FragmentScannerBinding? = null +class ScannerFragment : Fragment() { // This property is only valid between onCreateView and // onDestroyView. + private var _binding: FragmentScannerBinding? = null private val binding get() = _binding!! + private lateinit var navController: NavController + + // Camera + private var isBackCamera = true + private var frozenPreview: Boolean = false + private var cameraImageFile: File? = null + private lateinit var imageCapture: ImageCapture + private lateinit var cameraLauncher: ActivityResultLauncher<String> + + // Gallery + private lateinit var changeImage: ActivityResultLauncher<Intent> + private lateinit var pickImageLauncher: ActivityResultLauncher<String> + + // Upload + private lateinit var cacheDir: File + private lateinit var contentResolver: ContentResolver + private lateinit var apiClient: ApiClient + override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val scannerViewModel = - ViewModelProvider(this).get(ScannerViewModel::class.java) - _binding = FragmentScannerBinding.inflate(inflater, container, false) val root: View = binding.root - val textView: TextView = binding.textScanner - scannerViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + navController = findNavController() + cacheDir = requireContext().cacheDir + contentResolver = requireContext().contentResolver + apiClient = ApiClient() + + cameraLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + startCamera() + } + } + + changeImage = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + try { + val data = it.data + val imgUri = data?.data ?: throw Exception("Image Uri is null") + Log.d("ImageInput", "Image Selected with URI: $imgUri") + + // Copying image from external directory to cache + val tempFile = File.createTempFile("Gallery_Image", ".jpg", cacheDir) + val inputStream = contentResolver.openInputStream(imgUri) + val outputStream = FileOutputStream(tempFile) + inputStream?.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + + uploadPhoto(tempFile) + } catch (e: Exception) { + Log.e("ImageInput", "Image Input Failed:", e) + } + } + } + + val pickImageIntent = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI) + pickImageLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + changeImage.launch(pickImageIntent) + } + } + + imageCapture = ImageCapture.Builder().build() + cameraLauncher.launch(Manifest.permission.CAMERA) + + binding.switchCameraButton.setOnClickListener { + toggleCamera() + } + + binding.captureButton.setOnClickListener { + freezePreview() + } + + binding.galleryButton.setOnClickListener { + pickImageLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + binding.uploadButton.isClickable = false + binding.uploadButton.setOnClickListener { + cameraImageFile?.let { it1 -> uploadPhoto(it1) } } + return root } @@ -39,4 +146,108 @@ class ScannerFragment : Fragment() { super.onDestroyView() _binding = null } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder().build().also { mPreview -> + if (!frozenPreview) { + mPreview.setSurfaceProvider(binding.previewView.surfaceProvider) + } else { + mPreview.setSurfaceProvider(null) + } + } + + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + val cameraSelector = if (isBackCamera) { + CameraSelector.DEFAULT_BACK_CAMERA + } else { + CameraSelector.DEFAULT_FRONT_CAMERA + } + + try { + cameraProvider?.unbindAll() + cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture) + + val tempFile = File.createTempFile("Camera_Image", ".jpg", cacheDir) + val outputOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build() + imageCapture.takePicture(outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val uriImg = Uri.fromFile(tempFile) + cameraImageFile = tempFile + Log.d("CameraX", "Image captured: $uriImg") + } + + override fun onError(exception: ImageCaptureException) { + Log.e( + "CameraX", + "Error capturing image: ${exception.message}", + exception + ) + } + } + ) + } catch (e: Exception) { + Log.e("CameraX", "Starting Camera Failed:", e) + } + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun toggleCamera() { + isBackCamera = !isBackCamera + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun freezePreview() { + frozenPreview = !frozenPreview + if (!frozenPreview) { + binding.captureButton.text = "Capture" + binding.uploadButton.isClickable = false + } else { + binding.captureButton.text = "Retake" + binding.uploadButton.isClickable = true + } + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun uploadPhoto(photo: File) { + try { + val requestFile = photo.asRequestBody("image/*".toMediaTypeOrNull()) + val requestBody = MultipartBody.Part.createFormData("file", photo.name, requestFile) + val apiCall = apiClient.getApiService(requireContext()).getBill(requestBody) + apiCall.enqueue(object : Callback<BillResponse> { + override fun onResponse( + call: Call<BillResponse>, response: Response<BillResponse> + ) { + if (response.isSuccessful) { + val billResponse = + response.body() ?: throw Exception("Bill Response is Empty") + Log.d("BillUpload", "Server Response: $billResponse") + navigateScanResult(billResponse.items) + } else { + Log.e("BillUpload", "Error: ${response.code()}") + } + } + + override fun onFailure(call: Call<BillResponse>, t: Throwable) { + Log.e("BillUpload", "Failed to upload bill", t) + } + }) + } catch (e: Exception) { + Log.e("BillUpload", "Bill Upload Failed:", e) + } + } + + private fun navigateScanResult(items: Items) { + val scannerViewModel: ScannerViewModel by activityViewModels() + scannerViewModel.items = items.items + navController.navigate(R.id.action_to_scanResultFragment) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt index ba02a0bc2a2ac16f37abfefc81ad9afdc7cd6a0f..3e659568fd7e9ca0d091f8b24e1c7703777b3dc6 100644 --- a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt @@ -3,11 +3,14 @@ package com.example.bondoyap.ui.scanner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoyap.service.api.data.Item class ScannerViewModel : ViewModel() { + var items: List<Item>? = null private val _text = MutableLiveData<String>().apply { value = "This is Scanner Fragment" } val text: LiveData<String> = _text + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..15e214aa61d0894a9bc13b086ea6c9845571a7de --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import androidx.recyclerview.widget.DiffUtil +import com.example.bondoyap.service.api.data.Item + +class ItemDiffCallback : DiffUtil.ItemCallback<Item>() { + override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { + val isNameSame = oldItem.name == newItem.name + val isQuantitySame = oldItem.quantity == newItem.quantity + val isPriceSame = oldItem.price == newItem.price + return isNameSame && isQuantitySame && isPriceSame + } + + override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c751de3299b6cb36e794fe7c31cf07999f5943c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt @@ -0,0 +1,29 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R +import com.example.bondoyap.service.api.data.Item + +class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(item: Item) { + val itemName: TextView = itemView.findViewById(R.id.item_name) + val itemQuantity: TextView = itemView.findViewById(R.id.item_quantity) + val itemPrice: TextView = itemView.findViewById(R.id.item_price) + + itemName.text = "Nama: ${item.name}" + itemQuantity.text = "Jumlah: ${item.quantity}" + itemPrice.text = "Harga: ${item.price}" + } + + companion object { + fun create(parent: ViewGroup): ItemViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.recyclerview_scan_result, parent, false) + return ItemViewHolder(view) + } + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d60897087cbdcf1d42dc7263bc9495e249317a7e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.example.bondoyap.service.api.data.Item + +class ScanResultListAdapter : + ListAdapter<Item, ItemViewHolder>(ItemDiffCallback()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder.create(parent) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val currentItem = getItem(position) + holder.bind(currentItem) + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt index 9433c1071c0e9bc4cd8fcac9326e5c7b3acc43cc..4f415981d062e9ffd6f5e1af86636b4a812c704d 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsFragment.kt @@ -1,38 +1,133 @@ package com.example.bondoyap.ui.settings +import android.app.AlertDialog +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.example.bondoyap.databinding.FragmentSettingsBinding +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS +import com.example.bondoyap.ui.login.LoginActivity +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import android.Manifest class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! + private lateinit var settingsViewModel: SettingsViewModel + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val settingsViewModel = - ViewModelProvider(this).get(SettingsViewModel::class.java) - _binding = FragmentSettingsBinding.inflate(inflater, container, false) - val root: View = binding.root - val textView: TextView = binding.textSettings - settingsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE) + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val factory = context?.let { SettingsViewModelFactory(it) } + if (factory != null) { + settingsViewModel = ViewModelProvider(this, factory)[SettingsViewModel::class.java] + } else { + throw IllegalStateException("Context is null. Cannot create SettingsViewModelFactory.") + } + val textView = binding.textSettings + val logoutButton = binding.logoutButton + val randomButton = binding.randomTransactions + val saveButton = binding.saveTransactions + val sendButton = binding.sendTransactions + + settingsViewModel.getUser()?.let { user -> + val loggedInUserText = "Masuk dengan akun:\n ${user.email}" + textView.text = loggedInUserText + } + val appContext = context?.applicationContext + + logoutButton.setOnClickListener { + settingsViewModel.getUser() + settingsViewModel.logout() +// findNavController().navigate(R.id.navigation_login) + + val activity = requireActivity() + val intent = Intent(activity, LoginActivity::class.java) + Toast.makeText(appContext, "Logout Sukses!", Toast.LENGTH_SHORT).show() + activity.startActivity(intent) + activity.finish() } - return root + + randomButton.setOnClickListener { + Toast.makeText(appContext, "Membuat transaksi random ...", Toast.LENGTH_SHORT).show() + + val intent = Intent(ACTION_RANDOMIZE_TRANSACTIONS) + intent.putExtra("message", "Randomize from setting!") + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + + Log.d("BroadcastDebug", "Sending broadcast from SettingsFragment") + } + + val exporter = TransactionsExporter(transactionsViewModel, requireContext()) + + saveButton.setOnClickListener { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(appContext, "Allow storage permission untuk menyimpan transaksi ke file xls/xlsx", Toast.LENGTH_SHORT).show() + } else { + val formats = arrayOf("XLS", "XLSX") + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Pilih Format File") + builder.setItems(formats) { dialog: DialogInterface, which: Int -> + when (which) { + 0 -> { + Toast.makeText(appContext, "Menyimpan transaksi ke xls...", Toast.LENGTH_SHORT).show() + exporter.exportToXLS() + Toast.makeText(appContext, "Penyimpanan xls pada folder Documents berhasil...", Toast.LENGTH_SHORT).show() + } + 1 -> { + Toast.makeText(appContext, "Menyimpan transaksi ke xlsx...", Toast.LENGTH_SHORT).show() + exporter.exportToXLSX() + Toast.makeText(appContext, "Penyimpanan xlsx pada folder Documents berhasil...", Toast.LENGTH_SHORT).show() + } + } + dialog.dismiss() + } + builder.create().show() + } + } + + sendButton.setOnClickListener { + Toast.makeText(appContext, "Mengirimkan transaksi ...", Toast.LENGTH_SHORT).show() + //todo + } + + } + + companion object { + private const val PERMISSION_REQUEST_CODE = 1001 } override fun onDestroyView() { diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt index 02f1e625446ea7b7ced397b4d01dea34bc08185c..126d2f8867aa68b32386f7eb25275f64d1044596 100644 --- a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModel.kt @@ -3,11 +3,20 @@ package com.example.bondoyap.ui.settings import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoyap.ui.login.data.LoginRepository +import com.example.bondoyap.ui.login.data.model.LoggedInUser -class SettingsViewModel : ViewModel() { +class SettingsViewModel(private val loginRepository: LoginRepository) : ViewModel() { private val _text = MutableLiveData<String>().apply { value = "This is Settings Fragment" } val text: LiveData<String> = _text + + fun logout(){ + loginRepository.logout() + } + fun getUser(): LoggedInUser? { + return loginRepository.getUser() + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae367283563ca18363bc458743d2df24b2c51472 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/SettingsViewModelFactory.kt @@ -0,0 +1,23 @@ +package com.example.bondoyap.ui.settings + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.bondoyap.ui.login.data.LoginDataSource +import com.example.bondoyap.ui.login.data.LoginRepository + +class SettingsViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) { + return SettingsViewModel( + loginRepository = LoginRepository( + dataSource = LoginDataSource(context), + context = context + ) + ) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt b/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt new file mode 100644 index 0000000000000000000000000000000000000000..05c76159446de6ac39e1ac0589deff017b8bfe8c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/settings/TransactionsExporter.kt @@ -0,0 +1,98 @@ +package com.example.bondoyap.ui.settings + +import android.content.Context +import android.location.Address +import android.location.Geocoder +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.data.Transactions +import io.github.evanrupert.excelkt.Sheet +import io.github.evanrupert.excelkt.workbook +import kotlinx.coroutines.launch +import org.apache.poi.ss.usermodel.FillPatternType +import org.apache.poi.ss.usermodel.IndexedColors +import java.io.File +import java.util.Locale + +class TransactionsExporter(private val transactionsViewModel: TransactionsViewModel, val context: Context) { + fun exportToXLS() { + transactionsViewModel.viewModelScope.launch { + val transactions = transactionsViewModel.getAllTransactionsList() + Log.d("SaveDebug", "xls function") + writeToExcel("transactions.xls", transactions) + } + } + + fun exportToXLSX() { + transactionsViewModel.viewModelScope.launch { + val transactions = transactionsViewModel.getAllTransactionsList() + Log.d("SaveDebug", "xlsx function") + writeToExcel("transactions.xlsx", transactions) + } + } + + private fun writeToExcel(fileName: String, transactions: List<Transactions>) { + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + val file = File(downloadsDir, fileName) + + workbook { + sheet("Transactions") { + transactionsHeader() + + for (transaction in transactions) { + row { + cell(transaction.tanggal) + cell( + if (transaction.isPemasukan) { + "Pemasukan" + } else { + "Pengeluaran" + } + ) + cell(transaction.nominal) + cell(transaction.judul) + cell(if (transaction.longitude.isEmpty() || transaction.latitude.isEmpty()) { + "Unavailable" + } else { + val addresses: List<Address> = + Geocoder(context, Locale.getDefault()).getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + locationName + } else { + "Unavailable" + } + }) + } + } + } + }.write(file.absolutePath) + } + + + private fun Sheet.transactionsHeader() { + val headings = listOf("Tanggal", "Kategori Transaksi", "Nominal Transaksi", "Nama Transaksi", "Lokasi") + + val headingStyle = createCellStyle { + setFont(createFont { + fontName = "IMPACT" + color = IndexedColors.BLACK.index + }) + + fillPattern = FillPatternType.SOLID_FOREGROUND + fillForegroundColor = IndexedColors.YELLOW.index + } + + row(headingStyle) { + headings.forEach { cell(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..3179e81a058d04067bf17a8fb0807344e3d502bc --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt @@ -0,0 +1,128 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.example.bondoyap.databinding.FragmentAddTransactionsBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class AddTransactionsFragment : Fragment() { + + private var _binding: FragmentAddTransactionsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddTransactionsBinding.inflate(inflater, container, false) + + val pemasukan = "Pemasukan" + val pengeluaran = "Pengeluaran" + + val categories = arrayOf(pemasukan, pengeluaran) + val adapter = ArrayAdapter(requireContext(), R.layout.simple_spinner_item, categories) + + adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + + binding.buttonSimpan.setOnClickListener { + val isPemasukan: Boolean = when (binding.spinnerKategori.selectedItem.toString()) { + pemasukan -> true + else -> false + } + + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val currentDate = dateFormat.format(Date()) + + val judul = if (binding.editTextJudul.text.toString().trim().isNotEmpty()) { + binding.editTextJudul.text.toString() + } else { + "Untitled" + } + + val nominal = if (binding.editTextNominal.text.toString().trim().isNotEmpty()) { + binding.editTextNominal.text.toString().toDouble() + } else { + 0.0 + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("ScanResult", "Saving note on database") + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } + } + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + + Toast.makeText(requireContext(), "Transaksi berhasil disimpan", Toast.LENGTH_SHORT) + .show() + + binding.editTextJudul.text.clear() + binding.editTextNominal.text.clear() + binding.spinnerKategori.setSelection(0) + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0b9e61cd7c193ba3e5f41a0b3fb821c180a4094 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt @@ -0,0 +1,236 @@ +package com.example.bondoyap.ui.transactions + +import android.R +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.location.Address +import android.location.Geocoder +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.SpannableStringBuilder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.databinding.FragmentEditTransactionsBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import java.util.Locale + +class EditTransactionsFragment : Fragment() { + + private var _binding: FragmentEditTransactionsBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEditTransactionsBinding.inflate(inflater, container, false) + + var tanggal = "" + var latitude = "" + var longitude = "" + + val pemasukan = "Pemasukan" + val pengeluaran = "Pengeluaran" + + val categories = arrayOf(pemasukan, pengeluaran) + val adapter = ArrayAdapter(requireContext(), R.layout.simple_spinner_item, categories) + + adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) + binding.spinnerKategori.adapter = adapter + + binding.spinnerKategori.isEnabled = false + binding.editTextLokasi.isEnabled = false + + val transactionId: Int = arguments?.getInt("transaction_id") ?: -1 + + CoroutineScope(Main).launch { + + val originalTransaction: Transactions = transactionsViewModel.get(transactionId) + + binding.editTextJudul.text = SpannableStringBuilder(originalTransaction.judul) + binding.editTextNominal.text = + SpannableStringBuilder(originalTransaction.nominal.toBigDecimal().toString()) + + if (originalTransaction.isPemasukan) { + binding.spinnerKategori.setSelection(categories.indexOf(pemasukan)) + } else { + binding.spinnerKategori.setSelection(categories.indexOf(pengeluaran)) + } + + tanggal = originalTransaction.tanggal + longitude = originalTransaction.longitude + latitude = originalTransaction.latitude + + if (originalTransaction.longitude.isEmpty() || originalTransaction.latitude.isEmpty()) { + binding.editTextLokasi.text = SpannableStringBuilder("Unavailable") + } else { + val addresses: List<Address> = + Geocoder(requireContext(), Locale.getDefault()).getFromLocation( + originalTransaction.latitude.toDouble(), + originalTransaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + binding.editTextLokasi.text = SpannableStringBuilder(locationName) + } + } + } + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + binding.checkboxUpdateLokasi.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + if (!LocationManager.haveLocationPermission(requireContext())) { + binding.checkboxUpdateLokasi.isChecked = false + LocationManager.askLocationPermission(requireContext(), requireActivity()) + } + } + } + + binding.buttonUpdate.setOnClickListener { + val isPemasukan: Boolean = when (binding.spinnerKategori.selectedItem.toString()) { + pemasukan -> true + else -> false + } + + val judul = if (binding.editTextJudul.text.toString().trim().isNotEmpty()) { + binding.editTextJudul.text.toString() + } else { + "Untitled" + } + + val nominal = if (binding.editTextNominal.text.toString().trim().isNotEmpty()) { + binding.editTextNominal.text.toString().toDouble() + } else { + 0.0 + } + + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = tanggal, + longitude = longitude, + latitude = latitude, + id = transactionId + ) + + if (binding.checkboxUpdateLokasi.isChecked) { + @SuppressLint("MissingPermission") + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { loc -> + if (loc != null) { + latitude = loc.latitude.toString() + longitude = loc.longitude.toString() + + transaction.latitude = latitude + transaction.longitude = longitude + } + } + } + + showConfirmationDialog("Update", "Apakah Anda yakin ingin memperbarui transaksi ini?") { + transactionsViewModel.upsert(transaction) + + binding.editTextJudul.text = SpannableStringBuilder(transaction.judul) + binding.editTextNominal.text = + SpannableStringBuilder(transaction.nominal.toBigDecimal().toString()) + + if (transaction.longitude.isEmpty() || transaction.latitude.isEmpty()) { + binding.editTextLokasi.text = SpannableStringBuilder("Unavailable") + } else { + val addresses: List<Address> = + Geocoder(requireContext(), Locale.getDefault()).getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + binding.editTextLokasi.text = SpannableStringBuilder(locationName) + } + } + } + + if (transaction.isPemasukan) { + binding.spinnerKategori.setSelection(categories.indexOf(pemasukan)) + } else { + binding.spinnerKategori.setSelection(categories.indexOf(pengeluaran)) + } + Toast.makeText( + requireContext(), + "Transaksi berhasil diperbarui", + Toast.LENGTH_SHORT + ).show() + } + + + } + + binding.buttonHapus.setOnClickListener { + val transaction = Transactions( + judul = "", + nominal = 0.0, + isPemasukan = false, + tanggal = "", + id = transactionId + ) + + showConfirmationDialog("Hapus", "Apakah Anda yakin ingin menghapus transaksi ini?") { + transactionsViewModel.delete(transaction) + findNavController().navigate(com.example.bondoyap.R.id.navigation_transactions) + Toast.makeText(requireContext(), "Transaksi berhasil dihapus", Toast.LENGTH_SHORT) + .show() + } + } + + return binding.root + } + + private fun showConfirmationDialog(title: String, message: String, action: () -> Unit) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(title) + builder.setMessage(message) + builder.setPositiveButton("Ya") { _, _ -> + action.invoke() + } + builder.setNegativeButton("Tidak") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt new file mode 100644 index 0000000000000000000000000000000000000000..0634b8129e6625beb80b829d449e8f6219f26988 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsApplication.kt @@ -0,0 +1,10 @@ +package com.example.bondoyap.ui.transactions + +import android.app.Application +import com.example.bondoyap.ui.transactions.data.TransactionsRepository +import com.example.bondoyap.ui.transactions.data.TransactionsRoomDatabase + +class TransactionsApplication: Application() { + val database by lazy { TransactionsRoomDatabase.getDatabase(this) } + val repository by lazy { TransactionsRepository(database.transactionsDao()) } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa5d8fc4bf947d2ee073ebd84544518b6c806e9a --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsBroadcastReceiver.kt @@ -0,0 +1,83 @@ +package com.example.bondoyap.ui.transactions + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log +import android.widget.Toast +import androidx.core.app.ActivityCompat +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.random.Random + +class TransactionsBroadcastReceiver(private val transactionsViewModel: TransactionsViewModel) : BroadcastReceiver() { + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var latitude: String + private lateinit var longitude: String + + private val listJudulTransaksi = listOf( + "Belanja Bulanan", "Gajian", "THR", "Tagihan Listrik", "Tagihan Air", "Tagihan Internet", "Kebutuhan Dapur", "Pakaian", "Elektronik", "Makanan", + "Bahan Bakar", "Cicilan", "Asuransi", "Pajak", "Angsuran Kredit", "Tiket Transportasi", "Tiket Konser", "Biaya Pendidikan", "Sewa Rumah", "Tagihan Kartu Kredit", + "Gaji Bonus", "Infaq", "Zakat", "Donasi", "Uang Saku", "Tabungan", "Investasi", "Liburan", "Rekreasi", "Hadiah", + "Hutang Lunas", "Pinjaman Lunas", "Pensiun", "Bonus Tahunan", "Uang Jajan", "Royalti", "Hadiah Ulang Tahun", "Bayar Utang", "Refund", "Uang Lebaran", + "Uang Jalan", "Uang Makan", "Uang Sakit", "Uang Pemberian", "Uang Saku Anak", "Pensiun Dini", "Komisi", "Tunai Back" + ) + + override fun onReceive(context: Context?, intent: Intent?) { + Log.d("BroadcastDebug", "Broadcast received in AddTransactionsFragment") + + context ?: return + + val randomJudul = listJudulTransaksi[Random.nextInt(listJudulTransaksi.size)] + val randomNominal = Random.nextDouble(1000000000000000.0) + val randomIsPemasukan = Random.nextBoolean() + val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val currentDate = dateFormat.format(Date()) + + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + val location = fusedLocationProviderClient.lastLocation + location.addOnSuccessListener { + if(it != null) { + latitude = it.latitude.toString() + longitude = it.longitude.toString() + + val transaction: Transactions = Transactions( + judul = randomJudul, + nominal = randomNominal, + isPemasukan = randomIsPemasukan, + tanggal = currentDate, + longitude = longitude, + latitude = latitude + ) + transactionsViewModel.upsert(transaction) + } + } + + if( + ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + && ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) { + val transaction: Transactions = Transactions( + judul = randomJudul, + nominal = randomNominal, + isPemasukan = randomIsPemasukan, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + Toast.makeText(context.applicationContext, + "Izinkan location permission untuk membuat transaksi random dengan lokasi", + Toast.LENGTH_SHORT).show() + } + + Toast.makeText(context.applicationContext, "Transaksi random telah dibuat", Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt index b8cbc5f6ddde72493ea3dc5d9e81c53015e6557e..f928b30d81367d99044359eca8ad4d5c8754020a 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsFragment.kt @@ -1,42 +1,61 @@ package com.example.bondoyap.ui.transactions +import android.content.pm.PackageManager import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R import com.example.bondoyap.databinding.FragmentTransactionsBinding +import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS -class TransactionsFragment : Fragment() { - +class TransactionsFragment: Fragment() { private var _binding: FragmentTransactionsBinding? = null - // This property is only valid between onCreateView and - // onDestroyView. private val binding get() = _binding!! + private val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + private val requestcode: Int = 1 + override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val transactionsViewModel = - ViewModelProvider(this).get(TransactionsViewModel::class.java) - _binding = FragmentTransactionsBinding.inflate(inflater, container, false) - val root: View = binding.root - val textView: TextView = binding.textTransactions - transactionsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + val recyclerView = binding.root.findViewById<RecyclerView>(R.id.recyclerViewTransactions) + val adapter = TransactionsListAdapter(context = requireContext()) + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + transactionsViewModel.allTransactions.observe(viewLifecycleOwner, Observer { transactions -> + transactions?.let { adapter.submitList(it) } + }) + + binding.buttonAddTransaction.setOnClickListener { + findNavController().navigate(R.id.navigation_add_transactions) } - return root + + return binding.root } + override fun onDestroyView() { super.onDestroyView() _binding = null } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c5dce13bf504632c030d4a57aa4b6fc47b3991e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsListAdapter.kt @@ -0,0 +1,147 @@ +package com.example.bondoyap.ui.transactions + +import android.content.Context +import android.content.Intent +import android.location.Address +import android.location.Geocoder +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat.startActivity +import androidx.navigation.Navigation +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R +import com.example.bondoyap.ui.transactions.data.Transactions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.IOException +import java.util.Locale + +class TransactionsListAdapter(private val context: Context) : + ListAdapter<Transactions, TransactionsViewHolder>(TransactionsDiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransactionsViewHolder { + return TransactionsViewHolder.create(parent, context) + } + + override fun onBindViewHolder(holder: TransactionsViewHolder, position: Int) { + val currentTransaction = getItem(position) + holder.bind(currentTransaction) + } + +} + +class TransactionsViewHolder(itemView: View, private val context: Context) : RecyclerView.ViewHolder(itemView) { + private val cardView: CardView = itemView.findViewById(R.id.cardViewTransaction) + private val transactionTitle: TextView = itemView.findViewById(R.id.transactionTitle) + private val transactionAmount: TextView = itemView.findViewById(R.id.transactionAmount) + private val transactionCategory: TextView = itemView.findViewById(R.id.transactionCategory) + private val transactionDate: TextView = itemView.findViewById(R.id.transactionDate) + private val transactionLocation: TextView = itemView.findViewById(R.id.transactionLocation) + private val geocoder: Geocoder = Geocoder(context, Locale.getDefault()) + + fun bind(transaction: Transactions) { + val maxAmountLength = 12 + val maxTitleLength = 16 + val maxLocationLength = 9 + + val amountText = if (transaction.nominal.toBigDecimal().toString().length > maxAmountLength) { + "IDR " + transaction.nominal.toBigDecimal().toString().substring(0, maxAmountLength) + "..." + } else { + "IDR " + transaction.nominal.toBigDecimal().toString() + } + + val titleText = if (transaction.judul.length > maxTitleLength) { + transaction.judul.substring(0, maxTitleLength) + "..." + } else { + transaction.judul + } + + transactionTitle.text = titleText + transactionAmount.text = amountText + transactionCategory.text = when (transaction.isPemasukan) { + true -> "Pemasukan" + else -> "Pengeluaran" + } + + transactionDate.text = transaction.tanggal + + if (transaction.latitude.isNotEmpty() && transaction.longitude.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { + try { + val addresses: List<Address> = geocoder.getFromLocation( + transaction.latitude.toDouble(), + transaction.longitude.toDouble(), + 1 + ) ?: emptyList() + if (addresses.isNotEmpty()) { + val locationName = addresses[0].getAddressLine(0) + Handler(Looper.getMainLooper()).post { + transactionLocation.text = if (locationName.length > maxLocationLength) { + locationName.substring(0, maxLocationLength) + "..." + } else { + locationName + } + } + } + } catch (e: IOException) { + e.printStackTrace() + } + } + } else { + transactionLocation.text = "Unavailable" + } + + transactionLocation.setOnClickListener { + if (transactionLocation.text != "Unavailable") { + val mapUri = Uri.parse("https://maps.google.com/maps/search/?api=1&query=${transaction.latitude},${transaction.longitude}") + val intent = Intent(Intent.ACTION_VIEW, mapUri) + intent.setPackage("com.google.android.apps.maps") + if (intent.resolveActivity(context.packageManager) != null) { + startActivity(context, intent, null) + } else { + val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.com/maps/search/?api=1&query=${transaction.latitude},${transaction.longitude}")) + if (webIntent.resolveActivity(context.packageManager) != null) { + startActivity(context, webIntent, null) + } else { + Toast.makeText(context, "Tidak ada app yang dapat menghandle maps", Toast.LENGTH_SHORT).show() + } + } + } + } + + cardView.setOnClickListener { + val bundle: Bundle = Bundle() + bundle.putInt("transaction_id", transaction.id) + Navigation.findNavController(itemView).navigate(R.id.navigation_edit_transactions, bundle) + } + } + + companion object { + fun create(parent: ViewGroup, context: Context): TransactionsViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.recyclerview_transactions, parent, false) + return TransactionsViewHolder(view, context) + } + } +} + +class TransactionsDiffCallback : DiffUtil.ItemCallback<Transactions>() { + override fun areItemsTheSame(oldItem: Transactions, newItem: Transactions): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Transactions, newItem: Transactions): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt index 1a790c6f8709dc3c2f42d091b860cde6389501c3..3f5582da6b50d2bb0f8c9f03e2a5e644e783f247 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/TransactionsViewModel.kt @@ -1,13 +1,45 @@ package com.example.bondoyap.ui.transactions import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.example.bondoyap.ui.transactions.data.Transactions +import com.example.bondoyap.ui.transactions.data.TransactionsRepository +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.launch -class TransactionsViewModel : ViewModel() { +class TransactionsViewModel( + private val repository: TransactionsRepository +): ViewModel() { + val allTransactions: LiveData<List<Transactions>> = repository.allTransactions.asLiveData() - private val _text = MutableLiveData<String>().apply { - value = "This is Transactions Fragment" + fun upsert(transactions: Transactions) = viewModelScope.launch { + repository.upsert(transactions) + } + fun delete(transactions: Transactions) = viewModelScope.launch { + repository.delete(transactions) + } + suspend fun get(transactionId: Int?): Transactions { + val deferred: Deferred<Transactions> = viewModelScope.async { + repository.get(transactionId) + } + return deferred.await() + } + + suspend fun getAllTransactionsList(): List<Transactions> { + return repository.getAllTransactionsList() + } +} + +class TransactionsViewModelFactory(private val repository: TransactionsRepository): ViewModelProvider.Factory { + override fun<T: ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(TransactionsViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return TransactionsViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") } - val text: LiveData<String> = _text } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5bddbd9edcc1d9bc8323ad08c4ae69d6949e37d --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/Transactions.kt @@ -0,0 +1,30 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "transactions") +data class Transactions( + @ColumnInfo(name = "judul") + val judul: String, + + @ColumnInfo(name = "nominal") + val nominal: Double, + + @ColumnInfo(name = "is_pemasukan") + val isPemasukan: Boolean, + + @ColumnInfo(name = "tanggal") + val tanggal: String, + + @ColumnInfo(name = "longitude") + var longitude: String = "", + + @ColumnInfo(name = "latitude") + var latitude: String = "", + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + val id: Int = 0 +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..00dd1fc2a43c15e7ee6ac69ca1d725245c1b554c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsDao.kt @@ -0,0 +1,26 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransactionsDao { + + @Upsert + suspend fun upsertTransaction(transaksi: Transactions) + + @Delete + suspend fun deleteTransaction(transaksi: Transactions) + + @Query("SELECT * FROM transactions") + fun getTransactions(): Flow<List<Transactions>> + + @Query("SELECT * FROM transactions") + suspend fun getTransactionsList(): List<Transactions> + + @Query("SELECT * FROM transactions WHERE transactions.id == :transactionsId") + suspend fun getTransactionById(transactionsId: Int?): Transactions +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..c376411c484b935896c904d94217d7c0dc023b15 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRepository.kt @@ -0,0 +1,26 @@ +package com.example.bondoyap.ui.transactions.data + +import androidx.annotation.WorkerThread +import kotlinx.coroutines.flow.Flow + +class TransactionsRepository(private val transactionsDao: TransactionsDao) { + val allTransactions: Flow<List<Transactions>> = transactionsDao.getTransactions() + + @WorkerThread + suspend fun upsert(transactions: Transactions) { + transactionsDao.upsertTransaction(transactions) + } + + @WorkerThread + suspend fun delete(transactions: Transactions) { + transactionsDao.deleteTransaction(transactions) + } + + suspend fun get(transactionId: Int?): Transactions { + return transactionsDao.getTransactionById(transactionId) + } + + suspend fun getAllTransactionsList(): List<Transactions> { + return transactionsDao.getTransactionsList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..ada79cee07b3f11afee9b2a627791b737ab9c644 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/data/TransactionsRoomDatabase.kt @@ -0,0 +1,70 @@ +package com.example.bondoyap.ui.transactions.data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Database( + entities = [Transactions::class], + version = 3, + exportSchema = false +) +public abstract class TransactionsRoomDatabase: RoomDatabase() { + + abstract fun transactionsDao(): TransactionsDao + + companion object { + @Volatile + private var INSTANCE: TransactionsRoomDatabase? = null + + fun getDatabase(context: Context): TransactionsRoomDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + TransactionsRoomDatabase::class.java, + "transactions_database" + ).addMigrations(MIGRATION_2_3). + build() + INSTANCE = instance + instance + } + } + private val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `transactions_new` " + + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`judul` TEXT NOT NULL, " + + "`nominal` REAL NOT NULL, " + + "`is_pemasukan` INTEGER NOT NULL, " + + "`tanggal` TEXT NOT NULL, " + + "`lokasi` TEXT NOT NULL)") + + db.execSQL("INSERT INTO transactions_new (id, judul, nominal, is_pemasukan) " + + "SELECT id, judul, nominal, is_pemasukan FROM transactions") + + db.execSQL("DROP TABLE transactions") + + db.execSQL("ALTER TABLE transactions_new RENAME TO transactions") + } + } + private val MIGRATION_2_3: Migration = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `transactions_new` " + + "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`judul` TEXT NOT NULL, " + + "`nominal` REAL NOT NULL, " + + "`is_pemasukan` INTEGER NOT NULL, " + + "`tanggal` TEXT NOT NULL, " + + "`longitude` TEXT NOT NULL, " + + "`latitude` TEXT NOT NULL)") + + db.execSQL("DROP TABLE transactions") + + db.execSQL("ALTER TABLE transactions_new RENAME TO transactions") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pin.xml b/app/src/main/res/drawable/ic_pin.xml new file mode 100644 index 0000000000000000000000000000000000000000..4304772446917f94787245ca25f0020e4a904ccd --- /dev/null +++ b/app/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + + <path + android:fillColor="#FF000000" + android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5s-1.12,2.5 -2.5,2.5z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000000000000000000000000000000000..eb0f40356c67383e0760d86a3ad14bcd11220a7d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + + <path + android:fillColor="#000" + android:pathData="M19,13H13v6h-2v-6H5v-2h6V5h2v6h6v2z"/> + +</vector> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/activity_login.xml similarity index 81% rename from app/src/main/res/layout/fragment_login.xml rename to app/src/main/res/layout/activity_login.xml index 3835370f0dd243f7d2448961484710a245cdf4d5..cf194090807d97abade273046c604fba5498297e 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -2,21 +2,20 @@ <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/fragment_horizontal_margin" - android:paddingTop="@dimen/fragment_vertical_margin" - android:paddingRight="@dimen/fragment_horizontal_margin" - android:paddingBottom="@dimen/fragment_vertical_margin" - tools:context=".ui.login.LoginFragment"> + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".ui.login.LoginActivity"> <EditText android:id="@+id/email" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="24dp" android:layout_marginTop="96dp" - android:layout_marginEnd="24dp" android:autofillHints="@string/prompt_email" android:hint="@string/prompt_email" android:inputType="textEmailAddress" @@ -29,9 +28,7 @@ android:id="@+id/password" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="24dp" android:layout_marginTop="8dp" - android:layout_marginEnd="24dp" android:autofillHints="@string/prompt_password" android:hint="@string/prompt_password" android:imeActionLabel="@string/action_sign_in_short" @@ -47,9 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" - android:layout_marginStart="48dp" android:layout_marginTop="16dp" - android:layout_marginEnd="48dp" android:layout_marginBottom="64dp" android:enabled="false" android:text="@string/action_sign_in" @@ -64,9 +59,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginStart="32dp" android:layout_marginTop="64dp" - android:layout_marginEnd="32dp" android:layout_marginBottom="64dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" @@ -74,4 +67,5 @@ app:layout_constraintStart_toStartOf="@+id/password" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.3" /> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 06ea6cae22113f243efe317f984f7742418737e8..bad97d63dc8bac143e499f7ff3a349305c72700f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" - android:layout_height="match_parent" - android:paddingTop="?attr/actionBarSize"> + android:layout_height="match_parent"> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/nav_view" diff --git a/app/src/main/res/layout/fragment_add_transactions.xml b/app/src/main/res/layout/fragment_add_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7bc521ef2c2912a1614877a151b223a7d326f98 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_transactions.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_judul" /> + <EditText + android:id="@+id/editText_judul" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="judul" + android:hint="@string/hint_judul" + android:inputType="text" + android:maxLength="150" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_nominal" /> + <EditText + android:id="@+id/editText_nominal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="nominal" + android:hint="@string/hint_nominal" + android:inputType="numberDecimal" + android:maxLength="18" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_kategori" /> + <Spinner + android:id="@+id/spinner_kategori" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" /> + + <Button + android:id="@+id/button_simpan" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/button_simpan" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_edit_transactions.xml b/app/src/main/res/layout/fragment_edit_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea202d0be3584d842892ba196c00dd45967f3216 --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_transactions.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_judul" /> + <EditText + android:id="@+id/editText_judul" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="judul" + android:hint="@string/hint_judul" + android:inputType="text" + android:maxLength="150" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_nominal" /> + <EditText + android:id="@+id/editText_nominal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:autofillHints="nominal" + android:hint="@string/hint_nominal" + android:inputType="numberDecimal" + android:maxLength="18" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_kategori" /> + <Spinner + android:id="@+id/spinner_kategori" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/textfield_label_lokasi" /> + <EditText + android:id="@+id/editText_lokasi" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="text|textMultiLine" + android:gravity="top"/> + + <CheckBox android:id="@+id/checkbox_update_lokasi" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Perbarui lokasi" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/button_hapus" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="8dp" + android:text="@string/button_hapus" + android:backgroundTint="@color/red"/> + + <Button + android:id="@+id/button_update" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/button_update" /> + + </LinearLayout> + + + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_scan_result.xml b/app/src/main/res/layout/fragment_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..972c342f9703685caafa1ddf6265ee9e7d1cc188 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_result.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".ui.scanner.ScanResultFragment"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <!-- TODO: fix relative to bottom nav bar --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="72dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/cancel_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel_button" /> + + <Button + android:id="@+id/save_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/save_button" /> + + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/fragment_scanner.xml b/app/src/main/res/layout/fragment_scanner.xml index 883e701ac332ec99a2870cc89a74d2e6a979e81f..4caebe4c9e62ac89b85ed9966c7389d519e607b6 100644 --- a/app/src/main/res/layout/fragment_scanner.xml +++ b/app/src/main/res/layout/fragment_scanner.xml @@ -6,17 +6,45 @@ android:layout_height="match_parent" tools:context=".ui.scanner.ScannerFragment"> - <TextView - android:id="@+id/text_scanner" + + <androidx.camera.view.PreviewView + android:id="@+id/preview_view" android:layout_width="match_parent" + android:layout_height="400dp" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/switch_camera_button" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + android:text="@string/switch_camera_button" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/capture_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/capture_button" + app:layout_constraintStart_toEndOf="@+id/switch_camera_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/gallery_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/gallery_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/capture_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + + <Button + android:id="@+id/upload_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/upload_button" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/capture_button" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 5f90bc6adda3319a6073dd79512e10f163e686f2..61209ef2bcd9cd0308c8511663ffedf81705ba89 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -4,7 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.settings.SettingsFragment"> + tools:context=".ui.settings.SettingsFragment" + android:paddingTop="?attr/actionBarSize" + > <TextView android:id="@+id/text_settings" @@ -15,8 +17,51 @@ android:layout_marginEnd="8dp" android:textAlignment="center" android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"/> + + <Button + android:id="@+id/random_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/random_transactions" + app:layout_constraintTop_toBottomOf="@+id/text_settings" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/save_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/save_transactions" + app:layout_constraintTop_toBottomOf="@+id/random_transactions" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/send_transactions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/send_transactions" + app:layout_constraintTop_toBottomOf="@+id/save_transactions" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp" + /> + + <Button + android:id="@+id/logout_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/logout" + app:layout_constraintTop_toBottomOf="@+id/send_transactions" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="16dp"/> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_transactions.xml b/app/src/main/res/layout/fragment_transactions.xml index 45fe32c51b6329dc85f34563c32e86ecd2491f52..ad4fddd01c2a89be080305241b2b70399ef56d7a 100644 --- a/app/src/main/res/layout/fragment_transactions.xml +++ b/app/src/main/res/layout/fragment_transactions.xml @@ -1,22 +1,23 @@ -<?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" +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.transactions.TransactionsFragment"> + android:layout_marginTop="16dp"> - <TextView - android:id="@+id/text_transactions" + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerViewTransactions" android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="60dp"/> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/button_addTransaction" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_marginEnd="24dp" + android:layout_marginBottom="72dp" + android:src="@drawable/ic_plus"/> + +</RelativeLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/recyclerview_scan_result.xml b/app/src/main/res/layout/recyclerview_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..c172e9daa24a17d34ac12b0e3de9263d366866c3 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_scan_result.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/card_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:cardBackgroundColor="#af5eff" + app:cardCornerRadius="8dp" + app:cardElevation="4dp"> + + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/item_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_quantity" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_price" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + </LinearLayout> + +</androidx.cardview.widget.CardView> diff --git a/app/src/main/res/layout/recyclerview_transactions.xml b/app/src/main/res/layout/recyclerview_transactions.xml new file mode 100644 index 0000000000000000000000000000000000000000..03524715210c6a87729033adc340fe07537e8e47 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_transactions.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/cardViewTransaction" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:cardCornerRadius="8dp" + app:cardElevation="4dp" + android:clickable="true" + android:focusable="true" + app:cardBackgroundColor="#af5eff"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/transactionDate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Tanggal" /> + + <TextView + android:id="@+id/transactionTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="20sp" + android:textStyle="bold" + android:layout_marginTop="5dp" + android:layout_marginBottom="5dp" + android:text="Judul" /> + + <TextView + android:id="@+id/transactionAmount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Nominal" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/transactionCategory" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Kategori" /> + + <TextView + android:id="@+id/transactionLocation" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" + android:text="Lokasi" + android:layout_marginTop="36dp" + app:drawableLeftCompat="@drawable/ic_pin" /> + + </LinearLayout> + + </LinearLayout> + +</androidx.cardview.widget.CardView> diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..036d09bc5fd523323794379703c4a111d1e28a04 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> - <monochrome android:drawable="@drawable/ic_launcher_foreground" /> + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> </adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml index 6f3b755bf50c6b03d8714a9c6184705e6a08389f..036d09bc5fd523323794379703c4a111d1e28a04 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@drawable/ic_launcher_background" /> - <foreground android:drawable="@drawable/ic_launcher_foreground" /> - <monochrome android:drawable="@drawable/ic_launcher_foreground" /> + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> </adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ed54b070361593fb163c9e1d1860592e25d89e3e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..cf1b43d672103d65381c46eb071bd1416a35b81b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b35925334629c8bba6dc62f069edac0463537576 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-ldpi/ic_launcher.png b/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a69d6f5689fdc214b28bf5fecbf33ab7f2994d Binary files /dev/null and b/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..26e1a02af74c5fd9774f19d7b2822fd0f5908c68 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3a14dadae65b43d5bba7e2fa2038e1e0de1b3f09 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..db1f5f9390945f1e8b0376087c007e563ddde4da Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da081676d42f6c3f78a2c91e7bcedddedb..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc6a68df058b7f7b2a65b63ccb1187b2e1852e7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..991c94a3d9a5a55d6951ebb36e1846a1571bf11d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..41fdbefc963e433891dac0a49ba1ab4ec41fbc7d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b2152740a0a62351f1144cc19e7e322d7bc98ff8 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f036a47549d47db79c16788749dca10..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..667709b349b96ceada152a2461ea5eb11f1a13f3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..bbbdd222e1a09b79af898ec63e7ae778d8ec63c4 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f5083623b375139afb391af71cc533a7dd37..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..136e87df540dd155c093aaaa0c265356892b946c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6fa1074b79ccd52ef67ac15c5637e85..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..d150fa8feda7d2d53208aae66cf3cebb0843fc05 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..12c7745d046f89e7779d0eb3c4cb7354d995bfad Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37cbc3587421d6889eadd1d91fbf1994d4..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index efa5003d6cdb2778fc6190e8d2ef6edc22bf9d79..983df0358bc32b2943927cbc28b36223c5fe0927 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -5,11 +5,11 @@ android:id="@+id/mobile_navigation" app:startDestination="@+id/navigation_transactions"> - <fragment - android:id="@+id/navigation_login" - android:name="com.example.bondoyap.ui.login.LoginFragment" - android:label="@string/title_login" - tools:layout="@layout/fragment_transactions" /> + <!-- <fragment--> + <!-- android:id="@+id/navigation_login"--> + <!-- android:name="com.example.bondoyap.ui.login.LoginActivity"--> + <!-- android:label="@string/title_login"--> + <!-- tools:layout="@layout/fragment_transactions" />--> <fragment android:id="@+id/navigation_transactions" @@ -17,11 +17,35 @@ android:label="@string/title_transactions" tools:layout="@layout/fragment_transactions" /> + <fragment + android:id="@+id/navigation_add_transactions" + android:name="com.example.bondoyap.ui.transactions.AddTransactionsFragment" + android:label="@string/title_transactions" + tools:layout="@layout/fragment_add_transactions" /> + + <fragment + android:id="@+id/navigation_edit_transactions" + android:name="com.example.bondoyap.ui.transactions.EditTransactionsFragment" + android:label="@string/title_transactions" + tools:layout="@layout/fragment_edit_transactions" /> + <fragment android:id="@+id/navigation_scanner" android:name="com.example.bondoyap.ui.scanner.ScannerFragment" android:label="@string/title_scanner" - tools:layout="@layout/fragment_scanner" /> + tools:layout="@layout/fragment_scanner"> + + <action + android:id="@+id/action_to_scanResultFragment" + app:destination="@id/navigation_scan_result" /> + + </fragment> + + <fragment + android:id="@+id/navigation_scan_result" + android:name="com.example.bondoyap.ui.scanner.ScanResultFragment" + android:label="@string/title_scan_result" + tools:layout="@layout/fragment_scan_result" /> <fragment android:id="@+id/navigation_graph" diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e065116699dfedbfa503eb28fe432bf1447dd73 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">200dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f681ae11694396c167bee5b520246962495c34d --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="activity_horizontal_margin">48dp</dimen> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127d327620c93d2b2d00342a68e97b98a48d..5b69a31ff5fc8b40517d0e5624620e080be37343 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,6 @@ <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> + <color name="red">#FF0000</color> + <color name="ic_launcher_background">#f6bc2b</color> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8676af5c24e54b33fb20416b8377c02db9e60dfe..170df652f7855ce5cc6f9f8a650e558b427c700f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,14 +4,44 @@ <string name="title_scanner">Scanner</string> <string name="title_graph">Graph</string> <string name="title_settings">Pengaturan</string> - <!-- Strings related to login --> + <string name="title_scan_result">Scan Result</string> + <string name="title_login">Login</string> <string name="prompt_email">Email</string> <string name="prompt_password">Password</string> <string name="action_sign_in">Sign in</string> <string name="action_sign_in_short">Sign in</string> - <string name="welcome">"Welcome!"</string> - <string name="invalid_username">Not a valid email</string> - <string name="invalid_password">Password must be >5 characters</string> - <string name="login_failed">"Login failed"</string> + <string name="welcome">"Selamat Datang!"</string> + <string name="logged_in">Masuk dengan akun:</string> + <string name="invalid_username">Bukan email yang valid</string> + <string name="invalid_password">Password tidak boleh kosong</string> + <string name="login_failed">"Login gagal"</string> + <string name="logout">Logout</string> + <string name="title_activity_login">Login</string> + + <string name="random_transactions">Membuat transaksi random</string> + <string name="save_transactions">Simpan daftar transaksi</string> + <string name="send_transactions">Kirim daftar transaksi</string> + + <string name="textfield_label_judul">Judul</string> + <string name="textfield_label_nominal">Nominal</string> + <string name="textfield_label_kategori">Kategori</string> + <string name="textfield_label_lokasi">Lokasi</string> + <string name="button_simpan">Simpan</string> + <string name="button_hapus">Hapus</string> + <string name="button_update">Update</string> + <string name="hint_judul">Enter Judul</string> + <string name="hint_nominal">Enter Nominal</string> + <string name="hint_kategori">Enter Kategori</string> + <string name="hint_lokasi">Enter Lokasi</string> + <string name="pemasukan">Pemasukan</string> + <string name="pengeluaran">Pengeluaran</string> + + <string name="capture_button">capture</string> + <string name="switch_camera_button">switch camera</string> + <string name="gallery_button">gallery</string> + <string name="upload_button">upload</string> + + <string name="cancel_button">cancel</string> + <string name="save_button">save</string> </resources> \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 31ed43cc99341f5b95f5b15fd54f84d14a585002..96e26d4e6b245ef5a2d5b0bba7d68c7d716a61d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { id("com.android.application") version "8.3.0" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } \ No newline at end of file