diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c0c69e3274fb87f76893e21f8fed5ea0cf5e506a..c2372a42139987be686f0f4b8ca5e61a74bdcb81 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -101,4 +101,11 @@ dependencies { implementation("androidx.camera:camera-video:${cameraXVersion}") implementation("androidx.camera:camera-view:${cameraXVersion}") implementation("androidx.camera:camera-extensions:${cameraXVersion}") + + // Use okhttp3 + implementation("com.squareup.okhttp3:okhttp:4.7.2") + + // Use Apache POI + implementation("com.github.SUPERCILEX.poi-android:poi:3.17") + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d82221827d85a619e92762915204a67a5b81471..0bbf9ce3b2399bc07a119c0b1bccdabab1145cda 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,17 @@ <service android:name=".service.auth.TokenExpService" android:foregroundServiceType="dataSync"/> + + <provider + android:authorities="com.example.counter" + android:name="androidx.core.content.FileProvider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths" + /> + </provider> </application> </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/MainActivity.kt b/app/src/main/java/com/example/bondoman/MainActivity.kt index f483f4ec4dadeac7380e12cdcc5f862a2e95bb69..d4c92b5a33ed233cfb52803267cf36add9ff3c22 100644 --- a/app/src/main/java/com/example/bondoman/MainActivity.kt +++ b/app/src/main/java/com/example/bondoman/MainActivity.kt @@ -51,7 +51,7 @@ class MainActivity : AppCompatActivity(), MyBroadcastListener { // menu should be considered as top level destinations. val appBarConfiguration = AppBarConfiguration( setOf( - R.id.navigation_home, R.id.navigation_transactions, R.id.navigation_graph, R.id.navigation_settings, R.id.transactionFragment + R.id.navigation_home, R.id.navigation_transactions, R.id.navigation_graph, R.id.navigation_settings, R.id.transactionFragment, R.id.resultFragment ) ) diff --git a/app/src/main/java/com/example/bondoman/api/APIClient.kt b/app/src/main/java/com/example/bondoman/api/APIClient.kt index d47922e22c88da380c81f3f3af9e2745461d15fd..4d6d38d51597a0c2028b107a7be689c8a4198c06 100644 --- a/app/src/main/java/com/example/bondoman/api/APIClient.kt +++ b/app/src/main/java/com/example/bondoman/api/APIClient.kt @@ -2,6 +2,7 @@ package com.example.bondoman.api import com.example.bondoman.api.auth.login.LoginService import com.example.bondoman.api.auth.token.TokenService +import com.example.bondoman.api.scan.UploadService import com.example.bondoman.common.Constant import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -21,4 +22,8 @@ object APIClient { val tokenService: TokenService by lazy { retrofit.create(TokenService::class.java) } + + val uploadService: UploadService by lazy { + retrofit.create(UploadService::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/api/scan/UploadService.kt b/app/src/main/java/com/example/bondoman/api/scan/UploadService.kt new file mode 100644 index 0000000000000000000000000000000000000000..4927ab85cc7980d77c72c6b87eee47050f7fa147 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/api/scan/UploadService.kt @@ -0,0 +1,18 @@ +package com.example.bondoman.api.scan + +import com.example.bondoman.api.scan.dto.UploadResponse +import okhttp3.MultipartBody +import retrofit2.Call +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface UploadService { + @Multipart + @POST("api/bill/upload") + fun upload( + @Header("Authorization") token: String, + @Part file: MultipartBody.Part + ): Call<UploadResponse> +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/api/scan/dto/UploadResponse.kt b/app/src/main/java/com/example/bondoman/api/scan/dto/UploadResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..dd93c3ff4816a3a6a184fb6e68d0b20c41eeed98 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/api/scan/dto/UploadResponse.kt @@ -0,0 +1,9 @@ +package com.example.bondoman.api.scan.dto + +import com.example.bondoman.core.data.Items +import com.squareup.moshi.Json + +data class UploadResponse( + @Json(name = "items") + val items: Items +) \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/core/data/Item.kt b/app/src/main/java/com/example/bondoman/core/data/Item.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f9cbde8b48fb9cfbc5dcf6689d1ad675f842f50 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/core/data/Item.kt @@ -0,0 +1,14 @@ +package com.example.bondoman.core.data + +data class Item( + val name: String, + val qty: Int, + val price: Float +){ + constructor(parcelableItem: ParcelableItem) : this( + parcelableItem.name, + parcelableItem.qty, + parcelableItem.price + ) +} + diff --git a/app/src/main/java/com/example/bondoman/core/data/Items.kt b/app/src/main/java/com/example/bondoman/core/data/Items.kt new file mode 100644 index 0000000000000000000000000000000000000000..f7e12b7500d0466b63cbde2a844c2f7ee880aac6 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/core/data/Items.kt @@ -0,0 +1,5 @@ +package com.example.bondoman.core.data + +data class Items( + var items: List<Item> +) diff --git a/app/src/main/java/com/example/bondoman/core/data/ParcelableItem.kt b/app/src/main/java/com/example/bondoman/core/data/ParcelableItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..0942faf8ac786199fcf28ecf4d20f7c9d9f65517 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/core/data/ParcelableItem.kt @@ -0,0 +1,37 @@ +package com.example.bondoman.core.data + +import android.os.Parcel +import android.os.Parcelable + +data class ParcelableItem( + val name: String, + val qty: Int, + val price: Float +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString()!!, + parcel.readInt(), + parcel.readFloat() + ) { + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + parcel.writeInt(qty) + parcel.writeFloat(price) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator<ParcelableItem> { + override fun createFromParcel(parcel: Parcel): ParcelableItem { + return ParcelableItem(parcel) + } + + override fun newArray(size: Int): Array<ParcelableItem?> { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/core/repository/scan/UploadRepository.kt b/app/src/main/java/com/example/bondoman/core/repository/scan/UploadRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..1be01a56fbbf5edee48a9ad845ea667204ab07c0 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/core/repository/scan/UploadRepository.kt @@ -0,0 +1,33 @@ +package com.example.bondoman.core.repository.scan + +import android.util.Log +import com.example.bondoman.api.APIClient +import com.example.bondoman.api.scan.dto.UploadResponse +import com.example.bondoman.common.response.ResponseContract +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MultipartBody +import retrofit2.Response + +class UploadRepository { + suspend fun upload(token: String?, part: MultipartBody.Part): ResponseContract { + return withContext(Dispatchers.IO) { + try { + Log.d("Upload Repository", "Header: Bearer $token") + val response: Response<UploadResponse> = APIClient.uploadService.upload("Bearer " + token!!, part).execute() + + if (response.isSuccessful) { + Log.d("Upload Repository", "Upload Succeeded") + ResponseContract.Success(response.body()) + } else { + val errorBody = response.errorBody()?.string() + Log.d("Upload Repository", "Upload Failed: $errorBody") + ResponseContract.Error(errorBody!!) + } + } catch (e: Exception) { + Log.e("Upload Repository", "Upload Failed: $e") + ResponseContract.Error(e.toString()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/home/HomeViewModel.kt b/app/src/main/java/com/example/bondoman/ui/home/HomeViewModel.kt deleted file mode 100644 index 8f39eec286934b5909f3d814af50591c0e40ea62..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/bondoman/ui/home/HomeViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.bondoman.ui.home - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class HomeViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is home Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/result/ResultAdapter.kt b/app/src/main/java/com/example/bondoman/ui/result/ResultAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..c2b181de0d980571af4fe5275372ab59e1b20e35 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/result/ResultAdapter.kt @@ -0,0 +1,37 @@ +package com.example.bondoman.ui.result + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoman.core.data.Item +import com.example.bondoman.databinding.ItemTransactionBinding + +class ResultAdapter(var items: ArrayList<Item>): RecyclerView.Adapter<ResultAdapter.ResultHolder>() { + + inner class ResultHolder(val binding: ItemTransactionBinding): RecyclerView.ViewHolder(binding.root) { + fun bind(get: Item) { + binding.title.text = get.name + binding.content.text = get.qty.toString() + binding.date.text = get.price.toString() + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultHolder { + val binding = ItemTransactionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ResultHolder(binding) + } + + override fun getItemCount(): Int { + return items.size + } + + override fun onBindViewHolder(holder: ResultHolder, position: Int) { + holder.bind(items[position]) + } + + fun updateItems(newItems: ArrayList<Item>) { + items.clear() + items.addAll(newItems) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/result/ResultFragment.kt b/app/src/main/java/com/example/bondoman/ui/result/ResultFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..43f958a7e2fb40fe04114aa40bfd7fa96d338710 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/result/ResultFragment.kt @@ -0,0 +1,89 @@ +package com.example.bondoman.ui.result + +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 androidx.fragment.app.viewModels +import androidx.navigation.Navigation +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.bondoman.core.data.Item +import com.example.bondoman.core.data.ParcelableItem +import com.example.bondoman.core.data.Transaction +import com.example.bondoman.databinding.FragmentResultBinding + +class ResultFragment : Fragment() { + + private var _binding: FragmentResultBinding? = null + + private val binding get() = _binding!! + private val viewModel: ResultViewModel by viewModels() + + private val resultAdapter = ResultAdapter(arrayListOf()) + + private var tempTransaction = Transaction("", "", 0L, 0L, "",0.0,0.0) + + private lateinit var items: List<ParcelableItem> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.uploadRecyclerView.apply { + layoutManager = LinearLayoutManager(context) + adapter = resultAdapter + } + + arguments?.let{ + items = ResultFragmentArgs.fromBundle(it).items.toList() + val temp = ArrayList<Item>() + var sum = 0.0 + items.forEach(){ + temp.add(Item(it)) + sum += Item(it).price + } + val time:Long = System.currentTimeMillis() + tempTransaction.location = "Default" + tempTransaction.latitude = 0.0 + tempTransaction.longitude = 0.0 + tempTransaction.title = "Transaction Scanned" + tempTransaction.type = "PEMBELIAN" + tempTransaction.nominal = sum.toLong() + tempTransaction.creationTime = time + + Log.d("Result Fragment", items.toString()) + resultAdapter.updateItems(temp) + } + + binding.uploadResultButtonReject.setOnClickListener { + viewModel.saveTransaction(tempTransaction) + val action = ResultFragmentDirections.actionResultFragmentToNavigationHome() + Navigation.findNavController(binding.uploadRecyclerView).navigate(action) + } + + binding.uploadResultButtonAccept.setOnClickListener { + val action = ResultFragmentDirections.actionResultFragmentToNavigationHome() + Navigation.findNavController(binding.uploadRecyclerView).navigate(action) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = FragmentResultBinding.inflate(inflater, container, false) + val root: View = binding.root + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/result/ResultViewModel.kt b/app/src/main/java/com/example/bondoman/ui/result/ResultViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..1828e83a979c97e4ca319d5c79bb3087a248b345 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/result/ResultViewModel.kt @@ -0,0 +1,57 @@ +package com.example.bondoman.ui.result + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import com.example.bondoman.core.data.Transaction +import com.example.bondoman.core.repository.TransactionRepository +import com.example.bondoman.core.usecase.AddTransaction +import com.example.bondoman.core.usecase.GetAllTransaction +import com.example.bondoman.core.usecase.GetTransaction +import com.example.bondoman.core.usecase.GetTransactionTypeCount +import com.example.bondoman.core.usecase.RemoveTransaction +import com.example.bondoman.framework.RoomTransactionDataSource +import com.example.bondoman.framework.UseCases +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ResultViewModel(application: Application) : AndroidViewModel(application){ + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + val repository = TransactionRepository(RoomTransactionDataSource(application)) + + val useCases = UseCases( + AddTransaction(repository), + GetTransaction(repository), + GetAllTransaction(repository), + RemoveTransaction(repository), + GetTransactionTypeCount(repository) + ) + + var saved = MutableLiveData<Boolean>() + val currentTransaction = MutableLiveData<Transaction?>() + + fun saveTransaction(transaction: Transaction) { + coroutineScope.launch { + useCases.addTransaction(transaction) + Log.d("chane", "Observer triggered with yippie") + saved.postValue(true) + } + } + + fun getTransaction(id: Long){ + coroutineScope.launch { + val transaction = useCases.getTransaction(id) + currentTransaction.postValue(transaction) + } + } + + fun deleteTransaction(transaction: Transaction){ + coroutineScope.launch { + useCases.removeTransaction(transaction) + saved.postValue(true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt index c14fdc3fff6c2ec388eddba1e027f4a9b7c9374d..53cab4e3435d48a2bf6e6b7db1f110adb0ffb8d3 100644 --- a/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsFragment.kt @@ -1,21 +1,35 @@ package com.example.bondoman.ui.settings -import android.content.ComponentName import android.content.Intent -import android.content.SharedPreferences +import android.net.Uri import android.os.Bundle +import android.os.Environment import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.example.bondoman.MainActivity +import com.example.bondoman.core.data.Transaction import com.example.bondoman.databinding.FragmentSettingsBinding import com.example.bondoman.service.auth.TokenExpService import com.example.bondoman.share_preference.PreferenceManager import com.example.bondoman.ui.login.LoginActivity +import org.apache.poi.ss.usermodel.BorderStyle +import org.apache.poi.ss.usermodel.CellStyle +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.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class SettingsFragment : Fragment() { @@ -30,32 +44,213 @@ class SettingsFragment : Fragment() { private lateinit var preferenceManager: PreferenceManager + private fun createSheetHeader(cellStyle: CellStyle, sheet: Sheet) { + val row = sheet.createRow(0) + + val headerList = listOf("Tanggal", "Kategori", "Nominal", "Nama", "Lokasi") + + for ((index, value) in headerList.withIndex()) { + val columnWidth = 15 * 500 + + sheet.setColumnWidth(index, columnWidth) + + val cell = row.createCell(index) + + cell?.setCellValue(value) + + cell.cellStyle = cellStyle + } + } + + private fun addData( + rowIndex: Int, + transaction: Transaction, + cellStyle: CellStyle, + sheet: Sheet + ) { + val row = sheet.createRow(rowIndex) + + // Tanggal + val dateCell = row.createCell(0) + val dateFormat = SimpleDateFormat("HH:mm:ss yyyy-MM-dd", Locale.getDefault()) + dateCell?.setCellValue(dateFormat.format(Date(transaction.creationTime))) + dateCell.cellStyle = cellStyle + + // Kategori + val categoryCell = row.createCell(1) + categoryCell?.setCellValue(transaction.type) + categoryCell.cellStyle = cellStyle + + // Nominal + val nominalCell = row.createCell(2) + nominalCell?.setCellValue(transaction.nominal.toString()) + nominalCell.cellStyle = cellStyle + + // Nama + val nameCell = row.createCell(3) + nameCell?.setCellValue(transaction.title) + nameCell.cellStyle = cellStyle + + // Lokasi + val locationCell = row.createCell(4) + locationCell.setCellValue(transaction.location) + locationCell.cellStyle = cellStyle + } + + private fun createWorkbook(): Workbook { + val workbook = XSSFWorkbook() + + val dateFormat = SimpleDateFormat("HH-mm-ss-dd-MM-yyyy", Locale.getDefault()) + + val sheet: Sheet = workbook.createSheet("Transaksi-" + dateFormat.format(Date())) + + val headerCellStyle = workbook.createCellStyle() + headerCellStyle.fillForegroundColor = IndexedColors.SKY_BLUE.index + headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND) + headerCellStyle.setAlignment(HorizontalAlignment.CENTER) + headerCellStyle.setBorderTop(BorderStyle.MEDIUM) + headerCellStyle.setBorderBottom(BorderStyle.MEDIUM) + headerCellStyle.setBorderLeft(BorderStyle.MEDIUM) + headerCellStyle.setBorderRight(BorderStyle.MEDIUM) + + createSheetHeader(headerCellStyle, sheet) + + val cellStyle = workbook.createCellStyle() + cellStyle.setBorderTop(BorderStyle.MEDIUM) + cellStyle.setBorderBottom(BorderStyle.MEDIUM) + cellStyle.setBorderLeft(BorderStyle.MEDIUM) + cellStyle.setBorderRight(BorderStyle.MEDIUM) + + val transactions = viewModel.transactions.value!! + + transactions.forEachIndexed { index, transaction -> + addData(index + 1, transaction, cellStyle, sheet) + } + + return workbook + } + + private fun saveToExcel() { + val workbook = createWorkbook() + + val appDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + + val dateFormat = SimpleDateFormat("HH-mm-ss-dd-MM-yyyy", Locale.getDefault()) + + var fileName = "Transaksi-" + dateFormat.format(Date()) + + fileName += if (binding.radioButtonOptionXls.isChecked) { + ".xls" + } else { + ".xlsx" + } + + val excelFile = File(appDirectory, fileName) + + try { + val fileOut = FileOutputStream(excelFile) + workbook.write(fileOut) + fileOut.close() + + Toast.makeText( + requireContext(), + "Spreadsheet saved in $appDirectory/$fileName", + Toast.LENGTH_LONG + ).show() + + Log.d("Settings Fragment", "Saved successfully in $appDirectory/$fileName") + } catch (err: Exception) { + Log.d("Settings Fragment", err.toString()) + } + } + + private fun sendEmail() { + val workbook = createWorkbook() + + val suffix = if (binding.radioButtonOptionXls.isChecked) { + ".xls" + } else { + ".xlsx" + } + + val excelFile = File.createTempFile("Transaksi_", suffix, requireContext().cacheDir) + + try { + val fileOut = FileOutputStream(excelFile) + workbook.write(fileOut) + fileOut.close() + + Log.d("Settings Fragment", "Saved successfully") + } catch (err: Exception) { + Log.d("Settings Fragment", err.toString()) + } + + val fileUri: Uri = + FileProvider.getUriForFile(requireContext(), "com.example.counter", excelFile) + + var intent = Intent(Intent.ACTION_SEND) + .setType("application/excel") + // TODO: Set recipient emails + .putExtra(Intent.EXTRA_EMAIL, arrayOf("13521092@std.stei.itb.ac.id", "13521054@std.stei.itb.ac.id", "13521082@std.stei.itb.ac.id")) + .putExtra(Intent.EXTRA_SUBJECT, "Data Transaksi") + .putExtra(Intent.EXTRA_TEXT, "This email is generated automatically") + .putExtra(Intent.EXTRA_STREAM, fileUri) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + startActivity(Intent.createChooser(intent, "Send Mail")) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val viewModel = - ViewModelProvider(this).get(SettingsViewModel::class.java) - _binding = FragmentSettingsBinding.inflate(inflater, container, false) + binding.radioButtonOptionXls.isChecked = true + preferenceManager = context?.let { PreferenceManager(it) }!! val root: View = binding.root + viewModel = ViewModelProvider(this).get(SettingsViewModel::class.java) + viewModel.getTransactions() + return root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.button.setOnClickListener{ - val intent = Intent("com.example.bondoman.action") - intent.putExtra("message", "hello") - requireContext().sendBroadcast(intent) - Log.d("intent send", "ok") + System.setProperty( + "org.apache.poi.javax.xml.stream.XMLInputFactory", + "com.fasterxml.aalto.stax.InputFactoryImpl" + ) + System.setProperty( + "org.apache.poi.javax.xml.stream.XMLOutputFactory", + "com.fasterxml.aalto.stax.OutputFactoryImpl" + ) + System.setProperty( + "org.apache.poi.javax.xml.stream.XMLEventFactory", + "com.fasterxml.aalto.stax.EventFactoryImpl" + ) + + // Save spreadsheets + binding.settingsButtonSaveSpreadsheet.setOnClickListener { + Log.d("Settings Fragment", viewModel.transactions.value.toString()) + + saveToExcel() } + // Send email + binding.settingsButtonSendEmail.setOnClickListener { + Log.d("Settings Fragment", viewModel.transactions.value.toString()) + + sendEmail() + } + + // Logout binding.logoutButton.setOnClickListener { preferenceManager.removePref() Intent(requireContext(), TokenExpService::class.java).also { @@ -67,6 +262,7 @@ class SettingsFragment : Fragment() { requireActivity().finish() } } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt index 8bc36c8b0576ea978bda6f2e91eadc39b54f54f3..af69e4815d882e19fdadfe59adca5e2cc8170a05 100644 --- a/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/bondoman/ui/settings/SettingsViewModel.kt @@ -1,7 +1,51 @@ package com.example.bondoman.ui.settings +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoman.core.data.Transaction +import com.example.bondoman.core.repository.TransactionRepository +import com.example.bondoman.core.usecase.AddTransaction +import com.example.bondoman.core.usecase.GetAllTransaction +import com.example.bondoman.core.usecase.GetTransaction +import com.example.bondoman.core.usecase.GetTransactionTypeCount +import com.example.bondoman.core.usecase.RemoveTransaction +import com.example.bondoman.framework.RoomTransactionDataSource +import com.example.bondoman.framework.UseCases +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class SettingsViewModel : ViewModel() { - // TODO: Implement the ViewModel +class SettingsViewModel(application: Application) : AndroidViewModel(application) { + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + val repository = TransactionRepository(RoomTransactionDataSource(application)) + + val useCases = UseCases( + AddTransaction(repository), + GetTransaction(repository), + GetAllTransaction(repository), + RemoveTransaction(repository), + GetTransactionTypeCount(repository), + ) + + val transactions = MutableLiveData<List<Transaction>>() + + fun getTransactions() { + coroutineScope.launch { + val transactionList = useCases.getAllTransaction() + Log.d("Settings View Model", transactionList.toString()) + transactions.postValue(transactionList) + Log.d("Settings View Model", transactions.value.toString()) + } + } + + fun setTransactions(transactionList: List<Transaction>) { + transactions.value = transactionList + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoman/ui/home/HomeFragment.kt b/app/src/main/java/com/example/bondoman/ui/upload/UploadFragment.kt similarity index 65% rename from app/src/main/java/com/example/bondoman/ui/home/HomeFragment.kt rename to app/src/main/java/com/example/bondoman/ui/upload/UploadFragment.kt index f582dff7976c304f4b3b9a1d8b820dee8ec64d4d..746df5970ce60c69bd9081d9a6511213ef416fbd 100644 --- a/app/src/main/java/com/example/bondoman/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/example/bondoman/ui/upload/UploadFragment.kt @@ -1,41 +1,39 @@ -package com.example.bondoman.ui.home +package com.example.bondoman.ui.upload import android.Manifest import android.content.ContentValues import android.content.pm.PackageManager -import android.os.Build 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 android.widget.Toast -import androidx.activity.result.contract.ActivityResultContract 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.ImageProxy import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.video.Recorder -import androidx.camera.video.Recording -import androidx.camera.video.VideoCapture import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.example.bondoman.R +import androidx.navigation.Navigation +import com.example.bondoman.core.data.Item +import com.example.bondoman.core.data.ParcelableItem import com.example.bondoman.databinding.FragmentHomeBinding +import com.example.bondoman.share_preference.PreferenceManager +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import java.io.File import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import okhttp3.RequestBody.Companion.asRequestBody -typealias LumaListener = (luma: Double) -> Unit - -class HomeFragment : Fragment() { +class UploadFragment : Fragment() { private var _binding: FragmentHomeBinding? = null @@ -43,6 +41,12 @@ class HomeFragment : Fragment() { // onDestroyView. private val binding get() = _binding!! + private lateinit var preferenceManager: PreferenceManager + + private lateinit var viewModel: UploadViewModel + + private lateinit var file: File + private var imageCapture: ImageCapture? = null private lateinit var cameraExecutor: ExecutorService @@ -59,15 +63,22 @@ class HomeFragment : Fragment() { } if (!permissionGranted) { Toast.makeText( - requireContext(), - "Permission request denied", - Toast.LENGTH_SHORT + requireContext(), "Permission request denied", Toast.LENGTH_SHORT ).show() } else { startCamera() } } + private fun goToUploadResultFragment() { + val items: Array<Item> = viewModel.items.value!!.items.toTypedArray() + val parcelableItem: Array<ParcelableItem> = items.map { + ParcelableItem(it.name, it.qty, it.price) + }.toTypedArray() + val action = UploadFragmentDirections.actionNavigationHomeToResultFragment(parcelableItem) + Navigation.findNavController(binding.cameraView).navigate(action) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -79,7 +90,32 @@ class HomeFragment : Fragment() { } // Set up the listeners to take photo and video capture buttons - binding.imageCaptureButton.setOnClickListener { takePhoto() } + binding.imageCaptureButton.setOnClickListener { + takePhoto() + + val mediaType = "image/jpeg" + + val part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody(mediaType.toMediaType()) + ) + + val token = preferenceManager.getToken() + + viewModel.upload(token, part) + } + + viewModel.uploadResponse.observe(viewLifecycleOwner) { res -> + Log.d("Upload Fragment", res.toString()) + viewModel.setItems(res) + + goToUploadResultFragment() + } + + viewModel.errorMessage.observe(viewLifecycleOwner) { res -> + Log.d("Upload Fragment", res.toString()) + } cameraExecutor = Executors.newSingleThreadExecutor() } @@ -89,25 +125,18 @@ class HomeFragment : Fragment() { val imageCapture = imageCapture ?: return // Create time stamped name and MediaStory entry - val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) - .format(System.currentTimeMillis()) + val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") } // Create output options object which contains file + metadata - val outputOptions = ImageCapture.OutputFileOptions - .Builder( - requireContext().contentResolver, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - contentValues - ) - .build() + file = File.createTempFile("TEMP_FILE_", ".jpg", requireContext().cacheDir) + val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build() // Set up image capture listener, which is triggered after photo has been taken - imageCapture.takePicture( - outputOptions, + imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(requireContext()), object : ImageCapture.OnImageSavedCallback { override fun onError(exception: ImageCaptureException) { @@ -116,11 +145,14 @@ class HomeFragment : Fragment() { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { val msg = "Photo captured successfully: ${outputFileResults.savedUri}" + + // TODO: remove Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) + + Log.d("Upload Fragment", file.name) } - } - ) + }) } private fun startCamera() { @@ -131,15 +163,13 @@ class HomeFragment : Fragment() { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // Preview - val preview = Preview.Builder() - .build() - .also { - it.setSurfaceProvider(binding.cameraView.surfaceProvider) - } + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(binding.cameraView.surfaceProvider) + } - imageCapture = ImageCapture.Builder() - .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) - .build() + imageCapture = + ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() // Select back camera as default val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA @@ -182,13 +212,15 @@ class HomeFragment : Fragment() { } override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentHomeBinding.inflate(inflater, container, false) val root: View = binding.root + preferenceManager = PreferenceManager(requireContext()) + + viewModel = ViewModelProvider(this).get(UploadViewModel::class.java) + return root } diff --git a/app/src/main/java/com/example/bondoman/ui/upload/UploadViewModel.kt b/app/src/main/java/com/example/bondoman/ui/upload/UploadViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a7c4bac4962d7a4f88a200ac4ea8f4855229578 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/ui/upload/UploadViewModel.kt @@ -0,0 +1,51 @@ +package com.example.bondoman.ui.upload + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.bondoman.api.scan.dto.UploadResponse +import com.example.bondoman.common.response.ResponseContract +import com.example.bondoman.core.data.Items +import com.example.bondoman.core.repository.scan.UploadRepository +import kotlinx.coroutines.launch +import okhttp3.MultipartBody + +class UploadViewModel : ViewModel() { + + private val uploadRepository = UploadRepository() + + private val _uploadResponse = MutableLiveData<UploadResponse>() + + val uploadResponse: LiveData<UploadResponse> = _uploadResponse + + private val _errorMessage = MutableLiveData<String>() + + val errorMessage: LiveData<String> = _errorMessage + + private val _items = MutableLiveData<Items>() + + val items: LiveData<Items> = _items + + fun upload(token: String?, part: MultipartBody.Part) { + viewModelScope.launch { + when (val result = uploadRepository.upload(token, part)) { + is ResponseContract.Success<*> -> { + _uploadResponse.value = (result.response as UploadResponse) + _items.value = _uploadResponse.value!!.items + Log.d("Upload View Model", items.value.toString()) + } + + is ResponseContract.Error -> { + _errorMessage.value = result.response + Log.d("Upload View Model", _errorMessage.value.toString()) + } + } + } + } + + fun setItems(res: UploadResponse) { + _items.value = res.items + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 5b995065ab12138dedff11c7dbe9a0a6eea04f35..e50be7fd32e2972d3d92ea8652ba6d5945c88f3d 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.home.HomeFragment"> + tools:context=".ui.upload.UploadFragment"> <androidx.camera.view.PreviewView android:id="@+id/camera_view" diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..d8a3f8c1d16360a6bfe126c238357a1212c92041 --- /dev/null +++ b/app/src/main/res/layout/fragment_result.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/frameLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ui.result.ResultFragment"> + + <!-- TODO: Update blank fragment layout --> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/upload_recycler_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="90dp" + app:layout_constraintBottom_toTopOf="@+id/upload_result_button_accept" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/upload_result_confirmation_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="65dp" + android:layout_marginBottom="33dp" + android:text="@string/upload_result_text_view_retake_note" + app:layout_constraintBottom_toTopOf="@+id/upload_result_button_accept" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.498" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/upload_recycler_view" /> + + <Button + android:id="@+id/upload_result_button_accept" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/upload_result_button_yes" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.498" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.595" /> + + <Button + android:id="@+id/upload_result_button_reject" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="40dp" + android:text="@string/upload_result_button_no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.498" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/upload_result_button_accept" /> + +</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 f1a39888229686f81a1420d16509b51416c6c7d3..e5cb59f27cf2c5ff7fc08e67af2c5b8bd1d694ef 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -6,22 +6,62 @@ android:layout_height="match_parent" tools:context=".ui.settings.SettingsFragment"> + <RadioGroup + android:id="@+id/radio_group" + android:layout_width="211dp" + android:layout_height="94dp" + android:layout_marginTop="40dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.11" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <RadioButton + android:id="@+id/radio_button_option_xls" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings_radio_button_xls" /> + + <RadioButton + android:id="@+id/radio_button_option_xlsx" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/settings_radio_button_xlsx" /> + + </RadioGroup> + <Button - android:id="@+id/button" + android:id="@+id/logout_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Button" - android:layout_margin="@dimen/standard_margin" + android:text="@string/signout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.069" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/settings_button_send_email" /> <Button - android:id="@+id/logout_button" + android:id="@+id/settings_button_send_email" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/signout" + android:text="@string/settings_button_send_email" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.084" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/settings_button_save_spreadsheet" /> + + <Button + android:id="@+id/settings_button_save_spreadsheet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/standard_margin" + android:text="@string/settings_button_save_spreadsheets" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.084" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.237" /> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index e683b7ecda97149c50016da248278e315934c0ba..dbe7f30468e71dc1f9eba2aff8b4c54cbe987a3b 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -7,9 +7,13 @@ <fragment android:id="@+id/navigation_home" - android:name="com.example.bondoman.ui.home.HomeFragment" + android:name="com.example.bondoman.ui.upload.UploadFragment" android:label="@string/title_home" - tools:layout="@layout/fragment_home" /> + tools:layout="@layout/fragment_home" > + <action + android:id="@+id/action_navigation_home_to_resultFragment" + app:destination="@id/resultFragment" /> + </fragment> <fragment android:id="@+id/navigation_transactions" @@ -47,5 +51,17 @@ android:name="com.example.bondoman.ui.settings.SettingsFragment" android:label="Settings" tools:layout="@layout/fragment_settings" /> + <fragment + android:id="@+id/resultFragment" + android:name="com.example.bondoman.ui.result.ResultFragment" + android:label="Scan Result" + tools:layout="@layout/fragment_result" > + <argument + android:name="items" + app:argType="com.example.bondoman.core.data.ParcelableItem[]" /> + <action + android:id="@+id/action_resultFragment_to_navigation_home" + app:destination="@id/navigation_home" /> + </fragment> </navigation> \ 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 fd8e42c5b4d05c9f92130d050fd26d4225e18b39..ffda2e62c6bede741b478e92c363c15f38a39ea2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,5 +15,12 @@ <string name="login_email_label">Email</string> <string name="signout">Logout</string> <string name="login_password_label">Password</string> + <string name="upload_result_text_view_retake_note">Retake Note?</string> + <string name="upload_result_button_yes">Yes</string> + <string name="upload_result_button_no">No</string> + <string name="settings_radio_button_xls">xls</string> + <string name="settings_radio_button_xlsx">xlsx</string> + <string name="settings_button_save_spreadsheets">Save as Spreadsheets</string> + <string name="settings_button_send_email">Send as Email</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..7581796d236252598cc3b49b2d0e38d0b5ceb7b8 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path + name="cache_path" + path="." /> +</paths> \ No newline at end of file