From 920a81cd9508e992140128964dd2ace858ec1891 Mon Sep 17 00:00:00 2001
From: "Moch. Sofyan Firdaus" <13521083@std.stei.itb.ac.id>
Date: Thu, 4 Apr 2024 02:40:09 +0700
Subject: [PATCH] feat: save excel to storage

---
 app/build.gradle.kts                          |   9 ++
 .../bondoman/room/TransactionCategory.kt      |   3 +-
 .../bondoman/ui/settings/SettingsFragment.kt  | 110 +++++++++++++++++-
 .../onionsquad/bondoman/util/Converters.kt    |   6 +-
 .../main/res/drawable/ic_baseline_save_24.xml |   5 +
 app/src/main/res/layout/fragment_settings.xml |  37 +++++-
 app/src/main/res/values/arrays.xml            |   7 ++
 app/src/main/res/values/strings.xml           |   1 +
 gradle/libs.versions.toml                     |   2 +
 9 files changed, 171 insertions(+), 9 deletions(-)
 create mode 100644 app/src/main/res/drawable/ic_baseline_save_24.xml
 create mode 100644 app/src/main/res/values/arrays.xml

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f50af94..adfc4c0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -34,6 +34,12 @@ android {
     buildFeatures {
         viewBinding = true
     }
+
+    packaging {
+        resources {
+            excludes += "META-INF/*"
+        }
+    }
 }
 
 dependencies {
@@ -67,4 +73,7 @@ dependencies {
 
     // Work
     implementation(libs.androidx.work.runtime)
+
+    // Excel
+    implementation(libs.excelkt)
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt
index a49c479..f37764b 100644
--- a/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionCategory.kt
@@ -2,5 +2,6 @@ package com.onionsquad.bondoman.room
 
 enum class TransactionCategory {
     INCOME,
-    OUTCOME
+    OUTCOME,
+    UNKNOWN
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt b/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt
index 37c78c8..671dffa 100644
--- a/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/ui/settings/SettingsFragment.kt
@@ -2,22 +2,65 @@ package com.onionsquad.bondoman.ui.settings
 
 import android.app.AlertDialog
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.ArrayAdapter
 import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.findNavController
 import com.onionsquad.bondoman.R
 import com.onionsquad.bondoman.auth.AutoLogoutWorker
 import com.onionsquad.bondoman.auth.SessionManager
 import com.onionsquad.bondoman.databinding.FragmentSettingsBinding
+import com.onionsquad.bondoman.repository.TransactionRepository
+import com.onionsquad.bondoman.room.TransactionCategory
+import com.onionsquad.bondoman.room.TransactionDatabase
+import com.onionsquad.bondoman.room.TransactionEntity
+import io.github.evanrupert.excelkt.workbook
+import org.apache.poi.ss.usermodel.FillPatternType
+import org.apache.poi.ss.usermodel.IndexedColors
+import java.io.OutputStream
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
 
 class SettingsFragment : Fragment() {
     private var _binding: FragmentSettingsBinding? = null
 
     private val binding get() = _binding!!
 
+    private val database by lazy { TransactionDatabase.getInstance(requireContext()) }
+    private val repository by lazy { TransactionRepository(database.transactionDao()) }
+
+
+    private val saveXls =
+        registerForActivityResult(CreateDocument("application/vnd.ms-excel")) { uri ->
+            if (uri != null) {
+                repository.listTransactions.observe(viewLifecycleOwner) { list ->
+                    Log.d(this::class.java.simpleName, "Saving transactions to $uri")
+                    requireContext().contentResolver.openOutputStream(uri)?.use { fos ->
+                        createExcelFile(list, fos)
+                    }
+                }
+            }
+        }
+
+    private val saveXlsx =
+        registerForActivityResult(
+            CreateDocument("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+        ) { uri ->
+            if (uri != null) {
+                repository.listTransactions.observe(viewLifecycleOwner) { list ->
+                    Log.d(this::class.java.simpleName, "Saving transactions to $uri")
+                    requireContext().contentResolver.openOutputStream(uri)?.use { fos ->
+                        createExcelFile(list, fos)
+                    }
+                }
+            }
+        }
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -26,7 +69,7 @@ class SettingsFragment : Fragment() {
         _binding = FragmentSettingsBinding.inflate(inflater, container, false)
 
         binding.apply {
-            logoutButton.setOnClickListener {
+            buttonLogout.setOnClickListener {
                 val alertBuilder = AlertDialog.Builder(requireContext())
                 alertBuilder.setTitle(R.string.title_alert_logout)
                 alertBuilder.setMessage(R.string.message_alert_logout)
@@ -39,6 +82,22 @@ class SettingsFragment : Fragment() {
                 }
                 alertBuilder.show()
             }
+
+            ArrayAdapter.createFromResource(
+                requireContext(),
+                R.array.excel_types,
+                android.R.layout.simple_spinner_dropdown_item
+            ).also { arrayAdapter ->
+                arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+                spinnerExcelType.adapter = arrayAdapter
+            }
+
+            buttonSave.setOnClickListener {
+                when (spinnerExcelType.selectedItem.toString()) {
+                    "XLS" -> saveXls.launch("transactions.xls")
+                    "XLSX" -> saveXlsx.launch("transactions.xlsx")
+                }
+            }
         }
 
         return binding.root
@@ -56,4 +115,53 @@ class SettingsFragment : Fragment() {
         findNavController().popBackStack(R.id.navigation_transaction, true)
         requireActivity().recreate()
     }
+
+    private fun createExcelFile(data: List<TransactionEntity>, outputStream: OutputStream) {
+        workbook {
+            val headers = arrayOf(
+                "Tanggal",
+                "Kategori Transaksi",
+                "Nominal Transaksi",
+                "Nama Transaksi",
+                "Lokasi"
+            )
+            val sheet = sheet {
+                val headingStyle = createCellStyle {
+                    val font = createFont { bold = true }
+                    setFont(font)
+                    fillPattern = FillPatternType.SOLID_FOREGROUND
+                    fillForegroundColor = IndexedColors.AQUA.index
+                }
+                row(headingStyle) {
+                    headers.forEach { cell(it) }
+                }
+                for (transaction in data) {
+                    row {
+                        val date = transaction.date
+                            .toInstant()
+                            .atZone(ZoneId.systemDefault())
+                            .toLocalDateTime()
+                            .format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"))
+                        cell(date)
+                        cell(transaction.category.text())
+                        cell(transaction.amount)
+                        cell(transaction.title)
+                        cell(transaction.location)
+                    }
+                }
+            }.xssfSheet
+            for (i in 0..headers.size) {
+                sheet.setColumnWidth(i, 30 * 256)
+            }
+        }.xssfWorkbook.write(outputStream)
+    }
+
+    private fun TransactionCategory.text(): String {
+        return when (this) {
+            TransactionCategory.INCOME -> "Pemasukan"
+            TransactionCategory.OUTCOME -> "Pengeluaran"
+            TransactionCategory.UNKNOWN -> ""
+        }
+
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt b/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt
index eb9f90a..ce6bedd 100644
--- a/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/util/Converters.kt
@@ -25,6 +25,10 @@ object Converters {
 
     @TypeConverter
     fun toTransactionCategory(categoryString: String): TransactionCategory {
-        return TransactionCategory.valueOf(categoryString)
+        return try {
+            TransactionCategory.valueOf(categoryString)
+        } catch (e: IllegalArgumentException) {
+            TransactionCategory.UNKNOWN
+        }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_save_24.xml b/app/src/main/res/drawable/ic_baseline_save_24.xml
new file mode 100644
index 0000000..cfd40f5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_save_24.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
+    
+</vector>
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 570a31e..21d5764 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -1,18 +1,43 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:orientation="vertical"
     tools:context=".ui.settings.SettingsFragment">
 
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/button_save"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.7"
+            android:backgroundTint="@color/white"
+            android:drawableLeft="@drawable/ic_baseline_save_24"
+            android:text="@string/action_save_transactions"
+            android:textAlignment="textStart"
+            android:textColor="@color/black" />
+
+        <Spinner
+            android:id="@+id/spinner_excel_type"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0.3" />
+    </LinearLayout>
+
     <Button
-        android:id="@+id/logout_button"
+        android:id="@+id/button_logout"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:textAlignment="textStart"
-        android:textColor="@color/design_default_color_error"
         android:backgroundTint="@color/white"
         android:drawableLeft="@drawable/ic_baseline_logout_24"
         android:drawableTint="@color/design_default_color_error"
-        android:text="@string/action_sign_out" />
-</FrameLayout>
\ No newline at end of file
+        android:text="@string/action_sign_out"
+        android:textAlignment="textStart"
+        android:textColor="@color/design_default_color_error" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..df45958
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string-array name="excel_types">
+        <item>XLS</item>
+        <item>XLSX</item>
+    </string-array>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 311f02b..9db0660 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
     <string name="log_out_success">Log out success</string>
     <string name="yes">Yes</string>
     <string name="no">No</string>
+    <string name="action_save_transactions">Save transaction</string>
 </resources>
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ee85886..5dc08ee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,6 +19,7 @@ retrofit = "2.11.0"
 okhttp3 = "4.12.0"
 annotation = "1.7.1"
 work = "2.9.0"
+excelkt = "1.0.2"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -43,6 +44,7 @@ retrofit2-converter-gson = { group = "com.squareup.retrofit2", name = "converter
 okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3"  }
 androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
 androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
+excelkt = { group = "io.github.evanrupert", name = "excelkt", version.ref = "excelkt" }
 
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }
-- 
GitLab