diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56e9f285d8cfdc6c270853a5d439021a278..312bf2eab8b229c84f94db3fd525d55fab3ad5ff 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="CompilerConfiguration"> - <bytecodeTargetLevel target="17" /> + <bytecodeTargetLevel target="20" /> </component> </project> \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c3383890637b4721df1f49d0b229e55c0f361..1026ee158eb30a72805ac4f497ab5e0636d20508 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,7 +3,20 @@ <component name="deploymentTargetDropDown"> <value> <entry key="app"> - <State /> + <State> + <runningDeviceTargetSelectedWithDropDown> + <Target> + <type value="RUNNING_DEVICE_TARGET" /> + <deviceKey> + <Key> + <type value="SERIAL_NUMBER" /> + <value value="adb-RRCT400LBLA-VDbMwR._adb-tls-connect._tcp" /> + </Key> + </deviceKey> + </Target> + </runningDeviceTargetSelectedWithDropDown> + <timeTargetWasSelectedWithDropDown value="2024-04-04T18:09:34.930937Z" /> + </State> </entry> </value> </component> diff --git a/.idea/misc.xml b/.idea/misc.xml index adb8ae0fea0131858229357ced529c35f718aa6f..42da8e87c91e9cb5366621afb4dec6a8a5750139 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> - <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK" /> + <component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="corretto-20" project-jdk-type="JavaSDK" /> </project> \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f26e135e320abd5bce93d7b3d016b155928d8d69..363f29ad30867f849d1c9bec9cbf6ffa743c4092 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -96,6 +96,9 @@ dependencies { //location implementation ("com.google.android.gms:play-services-location:21.2.0") + //xlsx + implementation("org.apache.poi:poi:5.1.0") + implementation("org.apache.poi:poi-ooxml:5.1.0") } kapt { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4442d140b8aa1ac2c355f8fbe1a74097212df88b..ff3d6e7d678942a1cb5f55ac0aa0249a3b280c3e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,8 @@ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <application android:name=".MainApp" @@ -36,14 +38,15 @@ android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="geo" /> - - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity diff --git a/app/src/main/java/com/pbd/psi/LoginActivity.kt b/app/src/main/java/com/pbd/psi/LoginActivity.kt index 57dd6a2d1176857986055bf9080f4caacb4c3f24..0298a8425f47aaf3bdae3611c1eb3d487170463e 100644 --- a/app/src/main/java/com/pbd/psi/LoginActivity.kt +++ b/app/src/main/java/com/pbd/psi/LoginActivity.kt @@ -38,6 +38,23 @@ class LoginActivity : AppCompatActivity() { isLogin() + viewModel.authResult.observe(this) { + when (it) { + is BaseResponse.Success -> { + val intentRecycle = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intentRecycle) + finish() + } + + is BaseResponse.Error -> { + Toast.makeText(this, "Token expired", Toast.LENGTH_SHORT).show() + } + else -> { + Toast.makeText(this, "Token expired", Toast.LENGTH_SHORT).show() + } + } + } + viewModel.loginResult.observe(this) { when (it) { is BaseResponse.Loading -> { @@ -84,7 +101,7 @@ class LoginActivity : AppCompatActivity() { Toast.makeText(this, "Email and password must be filled", Toast.LENGTH_SHORT).show() return } - viewModel.loginUser(email, password) + viewModel.loginUser("13521099@std.stei.itb.ac.id", "password_13521099") } private fun isOnline(context: Context) { diff --git a/app/src/main/java/com/pbd/psi/LoginViewModel.kt b/app/src/main/java/com/pbd/psi/LoginViewModel.kt index 40e1054ce69b01cc23ff846c80437a1577cd57d0..2279ccf7856e9650b48b8d1d13f47747f8cc4e50 100644 --- a/app/src/main/java/com/pbd/psi/LoginViewModel.kt +++ b/app/src/main/java/com/pbd/psi/LoginViewModel.kt @@ -5,6 +5,7 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.pbd.psi.models.AuthRes import com.pbd.psi.models.BaseResponse import com.pbd.psi.models.LoginReq import com.pbd.psi.models.LoginRes @@ -14,6 +15,7 @@ import kotlinx.coroutines.launch class LoginViewModel(application: Application) : AndroidViewModel(application){ val userRepo = UserRepository() val loginResult : MutableLiveData<BaseResponse<LoginRes>> = MutableLiveData() + val authResult : MutableLiveData<BaseResponse<AuthRes>> = MutableLiveData() fun loginUser(email: String, password: String){ loginResult.value = BaseResponse.Loading() @@ -39,4 +41,23 @@ class LoginViewModel(application: Application) : AndroidViewModel(application){ } } } + fun checkAuth(token :String){ + viewModelScope.launch { + try { + val response = userRepo.checkAuth(token) + if(response.isSuccessful){ + Log.d("LoginViewModel", "berhasillll") + authResult.value = BaseResponse.Success(data = response.body()) + }else{ + Log.d("LoginViewModel", "gagal") + loginResult.value = BaseResponse.Error(msg = response.message()) + + } + Log.d("LoginViewModel", "checkAuth: ${response.body()}") + } catch (e: Exception) { + Log.d("LoginViewModel", "checkAuth: ${e.message}") + loginResult.value = BaseResponse.Error(msg = e.message) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pbd/psi/repository/GraphRepository.kt b/app/src/main/java/com/pbd/psi/repository/GraphRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa0ca31f4ac2988f16c8faca24dc8cbb9e5d3622 --- /dev/null +++ b/app/src/main/java/com/pbd/psi/repository/GraphRepository.kt @@ -0,0 +1,19 @@ +package com.pbd.psi.repository +import androidx.lifecycle.LiveData +import com.pbd.psi.room.AppDatabase +import com.pbd.psi.room.Category +import com.pbd.psi.room.TransactionEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class GraphRepository @Inject constructor(private val database: AppDatabase) { + suspend fun getSumTransaction(category: Category): Int { + return withContext(Dispatchers.IO) { + // Perform database operation here + // For example: + database.transactionDao().getSumTransaction(category) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/pbd/psi/repository/SettingsRepository.kt b/app/src/main/java/com/pbd/psi/repository/SettingsRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d1a7a212977d7c4cb3ef3bf058890bcbd22ee1f --- /dev/null +++ b/app/src/main/java/com/pbd/psi/repository/SettingsRepository.kt @@ -0,0 +1,21 @@ +package com.pbd.psi.repository + +import android.util.Log +import androidx.lifecycle.LiveData +import com.pbd.psi.room.AppDatabase +import com.pbd.psi.room.TransactionEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SettingsRepository @Inject constructor(private val database: AppDatabase) { + val transactionList: LiveData<List<TransactionEntity>> = database.transactionDao().getAllTrans() + + suspend fun getAllTrans(): List<TransactionEntity> { + return withContext(Dispatchers.IO) { + Log.d("button_export", "Repo1") + Log.d("ResponseString", "Response: ${database.transactionDao().getAllTransactions()}") + database.transactionDao().getAllTransactions() + } + } +} diff --git a/app/src/main/java/com/pbd/psi/repository/UserRepository.kt b/app/src/main/java/com/pbd/psi/repository/UserRepository.kt index ee9f0c90215878d7362416e8df53bb945089039e..30ce5d137968a72b1b7b9c7149cc521fe84f70e0 100644 --- a/app/src/main/java/com/pbd/psi/repository/UserRepository.kt +++ b/app/src/main/java/com/pbd/psi/repository/UserRepository.kt @@ -1,13 +1,17 @@ package com.pbd.psi.repository import com.pbd.psi.api.ApiConfig +import com.pbd.psi.models.AuthRes import com.pbd.psi.models.LoginReq import com.pbd.psi.models.LoginRes import retrofit2.Response class UserRepository { - suspend fun loginUser(loginReq : LoginReq) : Response<LoginRes> { return ApiConfig.getApiService().login(loginReq) } + + suspend fun checkAuth(token:String) : Response<AuthRes> { + return ApiConfig.getApiService().auth(token) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pbd/psi/room/TransactionDao.kt b/app/src/main/java/com/pbd/psi/room/TransactionDao.kt index 0c78ab639cdef4077e29dd4881f6e9ff85fa2b6a..5a3eeacd1a5d4f288a163e8dbc79bb84e81a69cf 100644 --- a/app/src/main/java/com/pbd/psi/room/TransactionDao.kt +++ b/app/src/main/java/com/pbd/psi/room/TransactionDao.kt @@ -13,9 +13,15 @@ interface TransactionDao { @Query("SELECT * FROM transactionTable") fun getAllTrans(): LiveData<List<TransactionEntity>> + @Query("SELECT * FROM transactionTable") + suspend fun getAllTransactions(): List<TransactionEntity> + @Query("SELECT * FROM transactionTable WHERE id=:id LIMIT 1") fun getTransById(id: Int): LiveData<TransactionEntity> + @Query("SELECT SUM(amount) as amount FROM transactionTable WHERE category=:category GROUP BY category") + fun getSumTransaction(category: Category): Int + @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun addTransaction(trans: TransactionEntity) diff --git a/app/src/main/java/com/pbd/psi/room/TransactionEntity.kt b/app/src/main/java/com/pbd/psi/room/TransactionEntity.kt index 37c876f8687cb0727efe4b93b919ad9b6599c60c..81e0bc58f9457a73e8f2cc8a0538691f8c287f67 100644 --- a/app/src/main/java/com/pbd/psi/room/TransactionEntity.kt +++ b/app/src/main/java/com/pbd/psi/room/TransactionEntity.kt @@ -33,4 +33,4 @@ data class TransactionEntity( @ColumnInfo(name = "latitude") val latitude: Double = 0.0, -) \ No newline at end of file +) diff --git a/app/src/main/java/com/pbd/psi/ui/graph/GraphFragment.kt b/app/src/main/java/com/pbd/psi/ui/graph/GraphFragment.kt index b9e9ccce109c4d5a87ef160a22f2b5fbc899e7af..c5c42a08fe25b17a1ddc758e1989039fc4737bce 100644 --- a/app/src/main/java/com/pbd/psi/ui/graph/GraphFragment.kt +++ b/app/src/main/java/com/pbd/psi/ui/graph/GraphFragment.kt @@ -1,110 +1,112 @@ package com.pbd.psi.ui.graph -import android.R import android.graphics.Color import android.graphics.Typeface import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import com.github.mikephil.charting.data.PieEntry +import androidx.fragment.app.viewModels import com.github.mikephil.charting.animation.Easing import com.github.mikephil.charting.data.PieData import com.github.mikephil.charting.data.PieDataSet +import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.formatter.PercentFormatter import com.github.mikephil.charting.utils.MPPointF import com.pbd.psi.databinding.FragmentGraphBinding +import com.pbd.psi.repository.GraphRepository +import com.pbd.psi.room.AppDatabase +import dagger.hilt.android.AndroidEntryPoint - +@AndroidEntryPoint class GraphFragment : Fragment() { private lateinit var binding: FragmentGraphBinding + private val entries: ArrayList<PieEntry> = ArrayList() + private val viewModel: GraphViewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment binding = FragmentGraphBinding.inflate(layoutInflater) + val pieChart = binding.pieChart pieChart.setUsePercentValues(true) - pieChart.getDescription().setEnabled(false) + pieChart.description.isEnabled = false pieChart.setExtraOffsets(5f, 10f, 5f, 5f) - - // on below line we are setting drag for our pie chart pieChart.setDragDecelerationFrictionCoef(0.95f) - - // on below line we are setting circle color and alpha pieChart.setTransparentCircleColor(Color.BLACK) pieChart.setTransparentCircleAlpha(110) - - // on below line we are setting hole radius - pieChart.setHoleRadius(58f) - pieChart.setTransparentCircleRadius(61f) - - // on below line we are setting center text - pieChart.setDrawCenterText(true) - - // on below line we are setting - // rotation for our pie chart - pieChart.setRotationAngle(0f) - - // enable rotation of the pieChart by touch - pieChart.setRotationEnabled(true) - pieChart.setHighlightPerTapEnabled(true) - - // on below line we are setting animation for our pie chart + pieChart.holeRadius = 58f + pieChart.transparentCircleRadius = 61f + pieChart.rotationAngle = 0f + pieChart.isRotationEnabled = true + pieChart.isHighlightPerTapEnabled = true pieChart.animateY(1400, Easing.EaseInOutQuad) - - // on below line we are disabling our legend for pie chart pieChart.legend.isEnabled = false pieChart.setEntryLabelColor(Color.WHITE) pieChart.setEntryLabelTextSize(12f) - // on below line we are creating array list and - // adding data to it to display in pie chart - val entries: ArrayList<PieEntry> = ArrayList() - entries.add(PieEntry(150000f)) - entries.add(PieEntry(50000f)) + viewModel.fetchIncomeTotal() + viewModel.fetchExpenseTotal() - // on below line we are setting pie data set - val dataSet = PieDataSet(entries, "Mobile OS") + viewModel.incomeTotal.observe(viewLifecycleOwner) { incomeTotal -> + Log.d("GraphFragment", "Income total: $incomeTotal") + binding.textPemasukanNominal?.text = incomeTotal.toString() + addEntries(incomeTotal.toFloat(),"Pemasukan") + updatePieChart(-65536) + } - // on below line we are setting icons. - dataSet.setDrawIcons(false) + viewModel.expenseTotal.observe(viewLifecycleOwner) { expenseTotal -> + Log.d("GraphFragment", "Expense total: $expenseTotal") + binding.textPengeluaranNominal?.text = expenseTotal.toString() + addEntries(expenseTotal.toFloat(),"Pengeluaran") + updatePieChart(-16711936) + } + + return binding.root + } + + private fun addEntries(value: Float,label: String) { + entries.add(PieEntry(value,label)) + } - // on below line we are setting slice for pie + private fun updatePieChart(color:Int) { + val pieChart = binding.pieChart + val dataSet = PieDataSet(entries, "Mobile OS") + dataSet.setDrawIcons(false) dataSet.sliceSpace = 3f dataSet.iconsOffset = MPPointF(0f, 40f) dataSet.selectionShift = 5f - - // add a lot of colors to list - val colors: ArrayList<Int> = ArrayList() - colors.add(-65536) - colors.add(-16711936) - - // on below line we are setting colors. + Log.d("Entries", entries.toString()) + val colors = ArrayList<Int>() + if(entries.count() == 2){ + if(entries.get(0).label == "Pemasukan"){ + colors.add(-16711936) + colors.add(-65536) + + } + else{ + colors.add(-65536) + colors.add(-16711936) + + } + } + else{ + colors.add(color) + } dataSet.colors = colors - - // on below line we are setting pie data set val data = PieData(dataSet) data.setValueFormatter(PercentFormatter()) data.setValueTextSize(15f) data.setValueTypeface(Typeface.DEFAULT_BOLD) data.setValueTextColor(Color.WHITE) - pieChart.setData(data) - - // undo all highlights + pieChart.data = data pieChart.highlightValues(null) - - pieChart.setHoleColor(Color.parseColor("#121433")); - - // loading chart + pieChart.setHoleColor(Color.parseColor("#121433")) pieChart.invalidate() - - // on below line we are setting slice for pie - dataSet.sliceSpace = 3f - dataSet.iconsOffset = MPPointF(0f, 40f) - dataSet.selectionShift = 5f - return binding.root } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/pbd/psi/ui/graph/GraphViewModel.kt b/app/src/main/java/com/pbd/psi/ui/graph/GraphViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3866ce8757ba52483d8e5898e07838eb7ba3b0f9 --- /dev/null +++ b/app/src/main/java/com/pbd/psi/ui/graph/GraphViewModel.kt @@ -0,0 +1,37 @@ +package com.pbd.psi.ui.graph + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pbd.psi.repository.GraphRepository +import com.pbd.psi.room.Category +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class GraphViewModel @Inject constructor(private val repository: GraphRepository) : ViewModel() { + private val _incomeTotal = MutableLiveData<Int>() + val incomeTotal: LiveData<Int> + get() = _incomeTotal + + private val _expenseTotal = MutableLiveData<Int>() + val expenseTotal: LiveData<Int> + get() = _expenseTotal + + fun fetchIncomeTotal() { + viewModelScope.launch { + _incomeTotal.value = repository.getSumTransaction(Category.INCOME) + Log.d("GraphViewModel", "fetchIncomeTotal: ${_incomeTotal.value}") + } + } + + fun fetchExpenseTotal() { + viewModelScope.launch { + _expenseTotal.value = repository.getSumTransaction(Category.EXPENSE) + Log.d("GraphViewModel", "fetchExpenseTotal: ${_expenseTotal.value}") + } + } +} diff --git a/app/src/main/java/com/pbd/psi/ui/scan/ScanFragment.kt b/app/src/main/java/com/pbd/psi/ui/scan/ScanFragment.kt index d4877576f50b73d98a53ede999ba3cab49fa5f85..7cedb14523eb03f561a6be0110eb9593b128475b 100644 --- a/app/src/main/java/com/pbd/psi/ui/scan/ScanFragment.kt +++ b/app/src/main/java/com/pbd/psi/ui/scan/ScanFragment.kt @@ -214,30 +214,39 @@ class ScanFragment : Fragment() { if (responseBody != null) { val responseString = responseBody.toString() Log.d("ResponseString", "Response: $responseString") - Toast.makeText(requireContext(), "Image uploaded successfully! Response: $responseString", Toast.LENGTH_LONG).show() + Toast.makeText(requireContext(), "Image Successfully Uploaded!", Toast.LENGTH_LONG).show() try { - val scanData = parseScanData(responseString) - for (item in scanData?.items?.items ?: emptyList()) { - val curDate = Date() - val transactionEntity = TransactionEntity( - 0, - item.name, - Category.EXPENSE, - item.price.toInt() * item.qty, - curDate, - "location", - 0.0, - 0.0, - ) - Log.d("jancok", "woyyyyyyyyyyyyyyy") - viewModel.addTransaction(transactionEntity) + val scanData = response.body()?.items?.items ?: emptyList() + val namesBuilder = StringBuilder() + var totalPrice = 0 + for (item in scanData) { + namesBuilder.append(item.name) + namesBuilder.append(", ") + totalPrice += item.price.toInt() * item.qty } + val concatenatedNames = if (namesBuilder.isNotEmpty()) { + namesBuilder.substring(0, namesBuilder.length - 2) + } else { + "" + } + val curDate = Date() + val transactionEntity = TransactionEntity( + 0, + concatenatedNames, + Category.EXPENSE, + totalPrice, + curDate, + "location", + 0.0, + 0.0 + ) + viewModel.addTransaction(transactionEntity) } catch (e: Exception) { Log.e("UploadError", "Error parsing response JSON", e) Toast.makeText(requireContext(), "Error parsing response JSON", Toast.LENGTH_SHORT).show() } } else { - Toast.makeText(requireContext(), "Image uploaded successfully!", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), "Image Successfully Uploaded!", Toast.LENGTH_SHORT).show() } previewMask() } else { @@ -257,16 +266,6 @@ class ScanFragment : Fragment() { _binding = null } - fun parseScanData(responseString: String): UploadRes? { - return try { - val jsonObject = JSONObject(responseString) - val gson = Gson() - gson.fromJson(jsonObject.toString(), UploadRes::class.java) - } catch (e: Exception) { - null - } - } - private fun isOnline(context: Context): Boolean{ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager diff --git a/app/src/main/java/com/pbd/psi/ui/settings/SettingsFragment.kt b/app/src/main/java/com/pbd/psi/ui/settings/SettingsFragment.kt index f0978d8a15ea8f865c32f3745e04e665e0bea3cc..11dc096f5094f6e8051eb4bb13c45e9e1648a089 100644 --- a/app/src/main/java/com/pbd/psi/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/pbd/psi/ui/settings/SettingsFragment.kt @@ -1,17 +1,33 @@ package com.pbd.psi.ui.settings +import android.app.AlertDialog import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.os.Bundle -import androidx.fragment.app.Fragment +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import com.pbd.psi.LoginActivity import com.pbd.psi.databinding.FragmentSettingsBinding +import com.pbd.psi.room.TransactionEntity +import com.pbd.psi.ui.transaction.SettingsViewModel +import dagger.hilt.android.AndroidEntryPoint +import org.apache.poi.ss.usermodel.FillPatternType +import org.apache.poi.ss.usermodel.HorizontalAlignment +import org.apache.poi.ss.usermodel.IndexedColors +import org.apache.poi.ss.usermodel.VerticalAlignment +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import kotlin.collections.ArrayList +@AndroidEntryPoint class SettingsFragment : Fragment() { companion object { @@ -22,14 +38,21 @@ class SettingsFragment : Fragment() { private lateinit var binding: FragmentSettingsBinding private lateinit var sharedpreferences: SharedPreferences + private lateinit var fileName: String + private val viewModel: SettingsViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { sharedpreferences = requireActivity().getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE) + binding = FragmentSettingsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - binding = FragmentSettingsBinding.inflate(layoutInflater) binding.btnKeluar.setOnClickListener { val editor = sharedpreferences.edit() editor.clear() @@ -40,6 +63,7 @@ class SettingsFragment : Fragment() { startActivity(intent) requireActivity().finish() } + binding.btnUploadHistory.setOnClickListener { val email = sharedpreferences.getString(EMAIL, "") val intentEmail = Intent(Intent.ACTION_SEND) @@ -47,13 +71,97 @@ class SettingsFragment : Fragment() { intentEmail.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) intentEmail.putExtra(Intent.EXTRA_SUBJECT, "Upload History") intentEmail.putExtra(Intent.EXTRA_TEXT, "Berikut ini laporan hasil transaksi akun $email :\n") - startActivity(Intent.createChooser(intentEmail, "Send Email")) } + binding.btnSettings.setOnClickListener { - // apply broadcast receiver + val exportOptions = arrayOf("Export as XLSX", "Export as XLS") + + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle("Export File Type") + builder.setItems(exportOptions) { _: DialogInterface?, which: Int -> + when (which) { + 0 -> { + fileName = "transaction_data.xlsx" + launchFilePicker() + } + 1 -> { + fileName = "transaction_data.xls" + launchFilePicker() + } + } + } + + val dialog = builder.create() + dialog.show() } + } - return binding.root + private fun launchFilePicker() { + filePickerLauncher.launch(null) + } + + private val filePickerLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + if (uri != null) { + val documentFile = DocumentFile.fromTreeUri(requireContext(), uri) + if (documentFile != null && documentFile.isDirectory) { + viewModel.transactionList.observe(viewLifecycleOwner) { transItems -> + val transactions = requireNotNull(transItems) { "Transaction list is null" } + val transList = ArrayList(transactions) + Log.d("TransactionList", "TransactionList: $transList") + exportTransactionsToExcel(transList, documentFile, fileName) + } + } else { + Toast.makeText(requireContext(), "Invalid directory", Toast.LENGTH_SHORT).show() + } + } + } + + private fun exportTransactionsToExcel(transactionEntities: List<TransactionEntity>, directory: DocumentFile, fileName: String) { + if (transactionEntities.isNotEmpty()) { + val workbook = XSSFWorkbook() + val sheet = workbook.createSheet("Transaction Data") + + val headerCellStyle = workbook.createCellStyle() + headerCellStyle.fillForegroundColor = IndexedColors.GREY_25_PERCENT.getIndex() + headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND) + headerCellStyle.alignment = HorizontalAlignment.CENTER + headerCellStyle.verticalAlignment = VerticalAlignment.CENTER + val headerFont = workbook.createFont() + headerFont.bold = true + headerCellStyle.setFont(headerFont) + + val headers = arrayOf("Date", "Name", "Category", "Amount", "Location") + val headerRow = sheet.createRow(0) + headers.forEachIndexed { index, headerText -> + val cell = headerRow.createCell(index) + cell.setCellValue(headerText) + cell.cellStyle = headerCellStyle + } + + transactionEntities.forEachIndexed { rowIndex, transaction -> + val dataRow = sheet.createRow(rowIndex + 1) + dataRow.createCell(0).setCellValue(transaction.date.toString()) + dataRow.createCell(1).setCellValue(transaction.name ?: "") + dataRow.createCell(2).setCellValue(transaction.category.toString()) + dataRow.createCell(3).setCellValue(transaction.amount.toDouble()) + dataRow.createCell(4).setCellValue(transaction.location ?: "") + } + + val file: DocumentFile + if(fileName === "transaction_data.xls"){ + file = directory.createFile("application/vnd.ms-excel", fileName)!! + }else{ + file = directory.createFile("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName)!! + } + val outputStream = requireContext().contentResolver.openOutputStream(file!!.uri) + workbook.write(outputStream) + workbook.close() + outputStream?.close() + + Toast.makeText(requireContext(), "Data Successfully Exported!", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(requireContext(), "No transaction data available", Toast.LENGTH_SHORT).show() + } } } diff --git a/app/src/main/java/com/pbd/psi/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/pbd/psi/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab496e3125b4c173d5d94a8f6fcd02dbeafa144c --- /dev/null +++ b/app/src/main/java/com/pbd/psi/ui/settings/SettingsViewModel.kt @@ -0,0 +1,31 @@ +package com.pbd.psi.ui.transaction + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pbd.psi.repository.SettingsRepository +import com.pbd.psi.room.Category +import com.pbd.psi.room.TransactionEntity +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val repository: SettingsRepository, +) : ViewModel() { + var transactionList: LiveData<List<TransactionEntity>> = repository.transactionList + +// fun fetchData() { +// viewModelScope.launch { +// _data = repository.getAllTrans() +// Log.d("SettingsViewModel", "fetchData: $_data") +// } +// } +} + diff --git a/app/src/main/res/drawable/background_login.png b/app/src/main/res/drawable/background_login.png index d813cd7bd3e6c58d9553e288461f210373455a1f..e7bfa753b733008254490829246dd0d0c24da22a 100644 Binary files a/app/src/main/res/drawable/background_login.png and b/app/src/main/res/drawable/background_login.png differ diff --git a/app/src/main/res/drawable/edit_text_border.xml b/app/src/main/res/drawable/edit_text_border.xml new file mode 100644 index 0000000000000000000000000000000000000000..c6adf8fc99763ad993521565665ed4eac435fb5a --- /dev/null +++ b/app/src/main/res/drawable/edit_text_border.xml @@ -0,0 +1,8 @@ +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="#E6FFFFFF" /> + <stroke + android:width="3dp" + android:color="@color/primaryBlue" /> + <corners android:radius="8dp" /> +</shape> diff --git a/app/src/main/res/drawable/keluar.png b/app/src/main/res/drawable/keluar.png index 897d53f241c4c79740d304e39d53dc7f52a25f8b..ca6f21a02ccd3047b6580dee442c7600cc150e3d 100644 Binary files a/app/src/main/res/drawable/keluar.png and b/app/src/main/res/drawable/keluar.png differ diff --git a/app/src/main/res/drawable/login_button.png b/app/src/main/res/drawable/login_button.png new file mode 100644 index 0000000000000000000000000000000000000000..ea7f9a3af97cf281416b12d94fe4df3948cbb018 Binary files /dev/null and b/app/src/main/res/drawable/login_button.png differ diff --git a/app/src/main/res/drawable/upload_history.png b/app/src/main/res/drawable/upload_history.png index 58fd5642d44764f68a1226edc086f28fe16a1959..494675e8acd0acee9779e99cc75387b067c25c2a 100644 Binary files a/app/src/main/res/drawable/upload_history.png and b/app/src/main/res/drawable/upload_history.png differ diff --git a/app/src/main/res/layout-land/fragment_graph.xml b/app/src/main/res/layout-land/fragment_graph.xml index cefe76e5b9c2524a72ce7aa695a21cbac7abbf55..622ea5cd775319296e2b0dd3ef9665fc49067a9d 100644 --- a/app/src/main/res/layout-land/fragment_graph.xml +++ b/app/src/main/res/layout-land/fragment_graph.xml @@ -1,51 +1,136 @@ <?xml version="1.0" encoding="utf-8"?> -<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.graph.GraphFragment"> + android:background="@color/primaryBlack" + tools:context=".ui.twibbon.TwibbonFragment"> - <!-- TODO: Update blank fragment layout --> - <TextView + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/twibbon_header" android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center_horizontal" - android:text="@string/graph_fragment" - android:textAlignment="center" /> + android:layout_height="wrap_content" + android:background="@color/primaryBlack" + app:layout_constraintTop_toTopOf="parent"> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <TextView + android:id="@+id/twibbon_header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="@font/montserrat" + android:text="Graph" + android:textColor="@color/white" + android:textSize="20sp" + android:layout_marginVertical="8dp" + android:layout_gravity="center" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + </com.google.android.material.appbar.AppBarLayout> <LinearLayout - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="horizontal" - android:layout_marginTop="50dp"> + android:id="@+id/frameLayout" + android:layout_width="411dp" + android:layout_height="500dp" + android:visibility="visible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintVertical_bias="0.4" + android:layout_marginBottom="100dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/twibbon_header" + android:orientation="vertical"> + <!-- PieChart --> <com.github.mikephil.charting.charts.PieChart android:id="@+id/pieChart" android:layout_width="300dp" android:layout_height="300dp" - android:layout_centerHorizontal="true" + android:layout_gravity="center_horizontal" android:layout_marginTop="50dp" /> + + <!-- Container for Image and Text --> <LinearLayout + android:paddingHorizontal="20dp" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="center_horizontal" + android:layout_marginTop="20dp"> + + <!-- Image --> + <ImageView + android:layout_width="20dp" + android:layout_height="20dp" + android:src="@drawable/icon_pemasukan"/> + + <!-- Text --> <TextView - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" - android:text="Pemasukan" /> + android:layout_weight="1" + android:text="Pemasukan" + android:textSize="18sp" + android:fontFamily="@font/montserrat" + android:textStyle="bold" + android:textColor="@color/white" + android:layout_marginStart="8dp"/> + + <!-- Nominal --> <TextView + android:id="@+id/text_pemasukan_nominal" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="10000" /> + android:text="10000" + android:textSize="18sp" + android:fontFamily="@font/montserrat" + android:textColor="@color/white" + android:layout_marginStart="8dp"/> + </LinearLayout> + <LinearLayout + android:paddingHorizontal="20dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="center_horizontal" + android:layout_marginTop="20dp"> + + <!-- Image --> + <ImageView + android:layout_width="20dp" + android:layout_height="20dp" + android:src="@drawable/icon_pengeluaran"/> + + <!-- Text --> <TextView - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:text="Pengeluaran"/> + android:layout_weight="1" + android:text="Pengeluaran" + android:textSize="18sp" + android:fontFamily="@font/montserrat" + android:textStyle="bold" + android:textColor="@color/white" + android:layout_marginStart="8dp"/> + <TextView + android:id="@+id/text_pengeluaran_nominal" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="5000" /> + android:text="10000" + android:textSize="18sp" + android:fontFamily="@font/montserrat" + android:textColor="@color/white" + android:layout_marginStart="8dp" + /> </LinearLayout> - </LinearLayout> -</FrameLayout> + </LinearLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 217c400562e12acaf43953e2d2c7555f04f0c3d1..d65e940d62660ebc245d56ae918cff6f4868be34 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -29,7 +29,7 @@ android:id="@+id/edt_login_email" android:layout_width="match_parent" android:layout_height="50dp" - android:background="@drawable/rounder_corner_edit_text" + android:background="@drawable/edit_text_border" android:inputType="textEmailAddress" android:textColor="@color/black" android:hint="Input your name" @@ -40,7 +40,7 @@ android:id="@+id/edt_login_pass" android:layout_width="match_parent" android:layout_height="50dp" - android:background="@drawable/rounder_corner_edit_text" + android:background="@drawable/edit_text_border" android:inputType="textPassword" android:layout_marginTop="10dp" android:hint="Type your password" @@ -48,19 +48,16 @@ android:layout_marginBottom="24dp" /> - <Button + <ImageButton android:id="@+id/btn_login" - android:layout_width="match_parent" - android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="20dp" - android:text="LOGIN" - android:background="@drawable/rounder_corner_submit" + android:background="@drawable/login_button" android:textSize="20sp" + android:layout_width="match_parent" + android:layout_height="52dip" android:layout_marginBottom="24dp" /> - - <TextView android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 353d9149e4c8164d85c1c7926dbe46d866a8a4ee..46c35f036ec8f263076cdf881f8597af1d970c6b 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -55,7 +55,7 @@ android:gravity="center"> <ImageButton android:id="@+id/btn_settings" - android:layout_width="350dp" + android:layout_width="match_parent" android:layout_height="88dp" android:background="@drawable/download_history" android:text="Settings"/> @@ -69,7 +69,7 @@ android:gravity="center"> <ImageButton android:id="@+id/btn_upload_history" - android:layout_width="350dp" + android:layout_width="match_parent" android:layout_height="88dp" android:background="@drawable/upload_history" android:text="Settings"/> @@ -83,7 +83,7 @@ android:gravity="center"> <ImageButton android:id="@+id/btn_keluar" - android:layout_width="350dp" + android:layout_width="match_parent" android:layout_height="88dp" android:background="@drawable/keluar" android:text="Settings"/>