diff --git a/.idea/misc.xml b/.idea/misc.xml index f1044879e31aa73a3e757a2f81a68e7c26bd222a..d7916a51794ccbea94e1583ffbde18195a4950cc 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ -<?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK"> diff --git a/app/src/main/java/com/atm/bondowowo/data/model/ScanRequest.kt b/app/src/main/java/com/atm/bondowowo/data/model/ScanRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..dcb80dd8a4f59d0b3454d46599dce8ddde61fbd8 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/model/ScanRequest.kt @@ -0,0 +1,8 @@ +package com.atm.bondowowo.data.model + +import okhttp3.MultipartBody + + +data class ScanRequest( + val file: MultipartBody.Part, +) \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/data/model/ScanResponse.kt b/app/src/main/java/com/atm/bondowowo/data/model/ScanResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..5161a9bc60de19bf293fcb2bcc7a12bab8483b22 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/data/model/ScanResponse.kt @@ -0,0 +1,15 @@ +package com.atm.bondowowo.data.model + +data class ScanResponse( + val items: ItemsWrapper +) + +data class ItemsWrapper( + val items: List<ScanItemData> +) + +data class ScanItemData( + val name: String, + val qty: Int, + val price: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt b/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt index a1a54404537b435ace213c5fa017bd0f236b989e..6acc07b1068a6bf8363fe04fb1f0242579b25782 100644 --- a/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt +++ b/app/src/main/java/com/atm/bondowowo/data/remote/ApiService.kt @@ -2,11 +2,16 @@ package com.atm.bondowowo.data.remote import com.atm.bondowowo.data.model.LoginRequest import com.atm.bondowowo.data.model.LoginResponse +import com.atm.bondowowo.data.model.ScanRequest +import com.atm.bondowowo.data.model.ScanResponse import com.atm.bondowowo.data.model.VerifyResponse +import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part interface ApiService { @POST("api/auth/login") @@ -14,4 +19,11 @@ interface ApiService { @POST("api/auth/token") suspend fun verifyToken(@Header("Authorization") jwtToken: String): Response<Any> + + @Multipart + @POST("api/bill/upload") + suspend fun uploadScan( + @Header("Authorization") jwtToken: String, + @Part file: MultipartBody.Part + ): Response<ScanResponse> } diff --git a/app/src/main/java/com/atm/bondowowo/ui/scan/ScanFragment.kt b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanFragment.kt index 760f78a17a8c9f837a714d41f0a7a558ecd9f047..2edfe3ca20816511b1b9952ff9a7514e07b5c1e3 100644 --- a/app/src/main/java/com/atm/bondowowo/ui/scan/ScanFragment.kt +++ b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanFragment.kt @@ -9,6 +9,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle +import android.os.Environment import android.provider.MediaStore import android.util.Log import android.view.LayoutInflater @@ -21,32 +22,41 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.atm.bondowowo.R +import com.atm.bondowowo.data.local.database.AppDatabase +import com.atm.bondowowo.data.repository.TransactionRepository import com.atm.bondowowo.ui.twibbon.TwibbonActivity +import com.atm.bondowowo.utils.UserPreferencesUtil import com.budiyev.android.codescanner.CodeScanner import com.budiyev.android.codescanner.CodeScannerView import com.budiyev.android.codescanner.DecodeCallback import com.google.zxing.BinaryBitmap -import com.google.zxing.ChecksumException -import com.google.zxing.FormatException import com.google.zxing.LuminanceSource import com.google.zxing.MultiFormatReader -import com.google.zxing.NotFoundException import com.google.zxing.RGBLuminanceSource import com.google.zxing.Reader import com.google.zxing.common.HybridBinarizer +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException import java.io.InputStream class ScanFragment : Fragment() { + private lateinit var scanViewModel: ScanViewModel + private lateinit var tvResult: TextView private var WRITE_EXTERNAL_STORAGE_PERMISSION_CODE: Int = 1 private var READ_EXTERNAL_STORAGE_PERMISSION_CODE: Int = 2 private val CAMERA_PERMISSION_REQUEST_CODE = 100 private lateinit var codeScanner: CodeScanner + private lateinit var scannedFile: File override fun onCreateView( inflater: LayoutInflater, @@ -58,6 +68,10 @@ class ScanFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val database = AppDatabase.getInstance(requireContext()) + val transactionRepository = TransactionRepository(database) + val scanViewModelFactory = ScanViewModelFactory(transactionRepository) + scanViewModel = ViewModelProvider(this, scanViewModelFactory)[ScanViewModel::class.java] val scannerView = view.findViewById<CodeScannerView>(R.id.scanner) tvResult = view.findViewById(R.id.tv_result) // @@ -154,18 +168,28 @@ class ScanFragment : Fragment() { requireActivity().runOnUiThread { val contents = it.text tvResult.text = contents - Toast.makeText(requireContext(), "Scanned: ${it.text}", Toast.LENGTH_LONG).show() - - val scanOptions = arrayOf<String>("Ya", "Tidak") - AlertDialog.Builder(requireContext()) - .setTitle("Scan Berhasil!\nApakah ingin mengulagi proses Scan?") - .setItems(scanOptions) { _, which -> - when (which) { - 0 -> codeScanner.startPreview() - 1 -> setToTransaction() + val convertedFile = saveByteArrayToFile(it.rawBytes) + if (convertedFile !== null) { + scannedFile = convertedFile + Toast.makeText(requireContext(), "Scanned: ${it.text}", Toast.LENGTH_LONG) + .show() + val scanOptions = arrayOf<String>("Ya", "Tidak") + AlertDialog.Builder(requireContext()) + .setTitle("Scan Berhasil!\nApakah ingin menyimpan\n proses scan?") + .setItems(scanOptions) { _, which -> + when (which) { + 0 -> setToTransaction() + 1 -> codeScanner.startPreview() + } } - } - .create().show() + .create().show() + } else { + Toast.makeText( + requireContext(), + "Failed to capture Image, please retry", + Toast.LENGTH_SHORT + ).show() + } } } scannerView.setOnClickListener { @@ -174,8 +198,19 @@ class ScanFragment : Fragment() { } private fun setToTransaction() { - Log.d("TODO", "next to transaction") - TODO("Not yet implemented") + val tokenJWT = UserPreferencesUtil.getJWT(requireContext()) + val requestFile = RequestBody.create(MediaType.parse("image/*"), scannedFile) + val imagePart = MultipartBody.Part.createFormData("file", scannedFile.name, requestFile) + if (tokenJWT != null) { + scanViewModel.uploadScan(tokenJWT, imagePart, requireContext(), scannedFile) + } else { + Toast.makeText( + requireContext(), + "Unable to get auth, please Log In again", + Toast.LENGTH_SHORT + ).show() + } + } private fun openGallery() { @@ -194,6 +229,7 @@ class ScanFragment : Fragment() { val imagePath = convertMediaUriToPath(imageUri) Log.d("ScanFragment", "Berhasil convertMediaUriToPath") val imgFile = File(imagePath) + scannedFile = imgFile scanImageQRCode(imgFile) Log.d("ScanFragment", "Berhasil scanImageQRCode") } else { @@ -212,6 +248,7 @@ class ScanFragment : Fragment() { } private fun scanImageQRCode(file: File) { + scannedFile = file val inputStream: InputStream = BufferedInputStream(FileInputStream(file)) val bitmap = BitmapFactory.decodeStream(inputStream) val decoded = scanQRImage(bitmap) @@ -230,8 +267,6 @@ class ScanFragment : Fragment() { val result: com.google.zxing.Result = reader.decode(bitmap) Log.d("scanQRImage", "berhasil yang bagian Result") contents = result.text - // TODO MASUKIN BE DISINI - tvResult.text = contents val scanOptions = arrayOf<String>("Ya", "Tidak") @@ -256,4 +291,22 @@ class ScanFragment : Fragment() { return contents } + fun saveByteArrayToFile(byteArray: ByteArray): File? { + val directory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val fileName = "temp_image_${System.currentTimeMillis()}.png" // Unique filename + val file = File(directory, fileName) + + try { + val outputStream = FileOutputStream(file) + outputStream.write(byteArray) + outputStream.flush() + outputStream.close() + return file + } catch (e: IOException) { + e.printStackTrace() + } + return null + } + } diff --git a/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModel.kt b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..1de35898dc56e7d5fd9957b9165ce90ddfed1b2e --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModel.kt @@ -0,0 +1,83 @@ +package com.atm.bondowowo.ui.scan + +import android.content.Context +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.atm.bondowowo.data.model.ScanItemData +import com.atm.bondowowo.data.model.Transaction +import com.atm.bondowowo.data.repository.TransactionRepository +import com.atm.bondowowo.utils.NetworkUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.MultipartBody +import java.io.File +import java.time.LocalDateTime + +class ScanViewModel( + private val transactionRepository: TransactionRepository +) : ViewModel() { + fun uploadScan(tokenJWT: String, imagePart: MultipartBody.Part, context: Context, file: File) { + viewModelScope.launch { + try { + val response = withContext(Dispatchers.IO) { + tokenJWT.let { + NetworkUtils.apiService.uploadScan( + "Bearer $it", + imagePart + ) + } + } + if (response.isSuccessful) { + val scanResponse = response.body()?.items?.items + val scanTransaction = scanResponse?.let { convertFromScanResponse(it) } + if (scanTransaction != null) { + transactionRepository.insertTransaction(scanTransaction) + Toast.makeText( + context, + "Successfully new transaction", + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + "Unable to add data to phone storage", + Toast.LENGTH_SHORT + ).show() + } + } else { + Toast.makeText( + context, + "Failed, please re-Login", + Toast.LENGTH_SHORT + ).show() + } + file.delete() + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(context, "Unable to connect server...", Toast.LENGTH_SHORT) + .show() + } + + } + } + + private fun convertFromScanResponse(itemList: List<ScanItemData>): Transaction { + val name = "Transaksi - " + itemList.map { it.name }.joinToString(", ") + val kategori = if (itemList.size % 2 == 0) { + "pemasukan" + } else { + "pengeluaran" + } + val nominal = itemList.fold(0.0) { acc, item -> acc + (item.qty * item.price) } + return Transaction( + null, + nama = name, + date = LocalDateTime.now(), + kategori = kategori, + nominal = nominal, + lokasi = "Toko ${itemList[0].name}" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModelFactory.kt b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModelFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..92b08dd6755da5852a939070354dff454bc28152 --- /dev/null +++ b/app/src/main/java/com/atm/bondowowo/ui/scan/ScanViewModelFactory.kt @@ -0,0 +1,19 @@ +package com.atm.bondowowo.ui.scan + +import com.atm.bondowowo.ui.scan.ScanViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.atm.bondowowo.data.repository.TransactionRepository + +class ScanViewModelFactory( + private val transactionRepository: TransactionRepository +) : ViewModelProvider.Factory { + + override fun <T : ViewModel> create(modelClass: Class<T>): T { + if (modelClass.isAssignableFrom(ScanViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return ScanViewModel(transactionRepository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/atm/bondowowo/ui/transaction/TransactionInputFragment.kt b/app/src/main/java/com/atm/bondowowo/ui/transaction/TransactionInputFragment.kt index e282b1b82785bef4f3f7f6c0cd8c0b0a86ecbd1c..cc2709cbd1508dd8460dad2e4163949c8a07aa4e 100644 --- a/app/src/main/java/com/atm/bondowowo/ui/transaction/TransactionInputFragment.kt +++ b/app/src/main/java/com/atm/bondowowo/ui/transaction/TransactionInputFragment.kt @@ -112,6 +112,7 @@ class TransactionInputFragment : Fragment() { this.listener = listener } + // Failed still broadcast reciever private val inputValueReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == "com.atm.bondowowo.RANDOM_INPUT_TRANSACTION") {