diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 749b7ce624bd3685ead0ea8c099a1986c807e786..badcd890cb8b664dac2c3edd8ba117ca644b62f2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,6 +62,8 @@ dependencies { kapt("androidx.room:room-compiler:2.5.0") androidTestImplementation ("androidx.room:room-testing:2.5.0") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation ("org.apache.poi:poi:5.2.4") + implementation ("org.apache.poi:poi-ooxml:5.2.4") implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") - + implementation ("com.squareup.okhttp3:okhttp:4.9.1") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d66e5469809975b468b20212dd0665b0b181801..e9c84d7765dd6ba21990362be955cabb8dcc23b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,9 +9,14 @@ <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + - <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" @@ -31,6 +36,7 @@ <activity android:name=".MainActivity" android:exported="false" /> + <activity android:name=".BondoManSplashScreen" android:exported="true" @@ -42,6 +48,17 @@ </intent-filter> </activity> <service android:name=".CheckJWTBackground"/> + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="com.example.android_hit.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + </application> + </manifest> \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/CheckJWTBackground.kt b/app/src/main/java/com/example/android_hit/CheckJWTBackground.kt index 47c2e036fc77e8d97973153ce58b50a16815dd6c..5cb54e18dcd497b92718812c51c0823c26650fc7 100644 --- a/app/src/main/java/com/example/android_hit/CheckJWTBackground.kt +++ b/app/src/main/java/com/example/android_hit/CheckJWTBackground.kt @@ -6,7 +6,6 @@ import android.os.Handler import android.os.IBinder import android.util.Log import com.example.android_hit.utils.TokenManager -import retrofit2.Response import kotlin.concurrent.thread class CheckJWTBackground:Service() { diff --git a/app/src/main/java/com/example/android_hit/DetailTransaction.kt b/app/src/main/java/com/example/android_hit/DetailTransaction.kt index 72f6558798dd2fa67947372784586a632ed06b9c..faf07d21a074c8514cebfb40c0e31c00a21ed00b 100644 --- a/app/src/main/java/com/example/android_hit/DetailTransaction.kt +++ b/app/src/main/java/com/example/android_hit/DetailTransaction.kt @@ -1,13 +1,18 @@ package com.example.android_hit import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.example.android_hit.databinding.FragmentDetailTransactionBinding import com.example.android_hit.room.TransactionDB import com.example.android_hit.room.TransactionEntity @@ -21,19 +26,45 @@ class DetailTransaction : Fragment() { private lateinit var database: TransactionDB private var category: String = "" + private lateinit var transactionReceiver: TransactionReceiver + private val RANDOMIZE_ACTION = "com.example.android_hit.RANDOMIZE_ACTION" + companion object { + var fragmentCounter = 0 + var amountInput = "" + } + + inner class TransactionReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == RANDOMIZE_ACTION) { + // Handle the broadcast message here + val data = intent.getStringExtra("hargaRandom") + Log.e("BROD", "Received randomize broadcast message $data") + if (data != null) { + amountInput = data + } + } + } + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + fragmentCounter++ _binding = FragmentDetailTransactionBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + database = TransactionDB.getInstance(requireContext()) + transactionReceiver = TransactionReceiver() + val filter = IntentFilter(RANDOMIZE_ACTION) + LocalBroadcastManager.getInstance(requireContext()).registerReceiver(transactionReceiver, filter) + + binding.radioExpense.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { category = "Expense" @@ -46,6 +77,9 @@ class DetailTransaction : Fragment() { } } + if(amountInput!=""){ + binding.inputAmount.setText(amountInput) + } val intent = requireActivity().intent.extras if (intent != null) { val id = intent.getInt("id", 0) @@ -144,8 +178,15 @@ class DetailTransaction : Fragment() { } } + + override fun onDestroyView() { super.onDestroyView() _binding = null + amountInput="" + if(fragmentCounter>1){ + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(transactionReceiver) + } + } } diff --git a/app/src/main/java/com/example/android_hit/HeaderNetwork.kt b/app/src/main/java/com/example/android_hit/HeaderNetwork.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5409f42afb2c1351f5f6160fe54b9049c590df8 --- /dev/null +++ b/app/src/main/java/com/example/android_hit/HeaderNetwork.kt @@ -0,0 +1,59 @@ +package com.example.android_hit + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [HeaderNetwork.newInstance] factory method to + * create an instance of this fragment. + */ +class HeaderNetwork : Fragment() { + // TODO: Rename and change types of parameters + private var param1: String? = null + private var param2: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + param1 = it.getString(ARG_PARAM1) + param2 = it.getString(ARG_PARAM2) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_header_network, container, false) + } + + companion object { + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment HeaderNetwork. + */ + // TODO: Rename and change types and number of parameters + @JvmStatic + fun newInstance(param1: String, param2: String) = + HeaderNetwork().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, param1) + putString(ARG_PARAM2, param2) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/MainActivity.kt b/app/src/main/java/com/example/android_hit/MainActivity.kt index d21ebcf5843889cf2319a10c7483e0639f1fd10b..80a96b5e03481e2115d43e0924555b2e0838f4be 100644 --- a/app/src/main/java/com/example/android_hit/MainActivity.kt +++ b/app/src/main/java/com/example/android_hit/MainActivity.kt @@ -1,7 +1,6 @@ package com.example.android_hit import android.app.AlertDialog -import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import androidx.appcompat.app.AppCompatActivity @@ -27,12 +26,27 @@ class MainActivity : AppCompatActivity() { private lateinit var userPref : UserManager private lateinit var sharedPref : TokenManager private lateinit var cryptoManager: CryptoManager + + private lateinit var dialog : AlertDialog + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) userPref = UserManager(this) sharedPref = TokenManager(this) cryptoManager = CryptoManager() + + val networkManager = NetworkManager(this) + var networkDialog: AlertDialog? = null + + networkManager.observe(this) { isConnected -> + if (!isConnected) { + networkDialog = showNetworkDialog() + } else { + networkDialog?.dismiss() + networkDialog = null + } + } setContentView(binding.root) setCurrentFragment(Transaction(), HeaderTransaction()) @@ -41,14 +55,22 @@ class MainActivity : AppCompatActivity() { binding.bottomNavigation?.setOnItemSelectedListener { when (it.itemId) { R.id.nav_transaction -> { + // if(!networkManager.value!!){ + // setCurrentFragment(NetworkError(), HeaderNetwork()) + // }else{ + // setCurrentFragment(Transaction(), HeaderTransaction()) + // } setCurrentFragment(Transaction(), HeaderTransaction()) } + R.id.nav_scan -> { - setCurrentFragment(Scan(), HeaderScan()) + setCurrentFragment(Scan(), HeaderScan()) } + R.id.nav_graphs -> { setCurrentFragment(Graphs(), HeaderGraphs()) } + R.id.nav_settings -> { setCurrentFragment(Settings(), HeaderSettings()) } @@ -75,6 +97,20 @@ class MainActivity : AppCompatActivity() { } } + private fun showNetworkDialog(): AlertDialog { + val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(this) + alertDialogBuilder.setTitle("No Internet Connection") + alertDialogBuilder.setMessage("Please check your internet connection") + alertDialogBuilder.setCancelable(false) + + alertDialogBuilder.setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() + } + + val alertDialog: AlertDialog = alertDialogBuilder.create() + alertDialog.show() + return alertDialog + } private fun setCurrentFragment(fragment: Fragment, header: Fragment) = supportFragmentManager.beginTransaction().apply { diff --git a/app/src/main/java/com/example/android_hit/NetworkError.kt b/app/src/main/java/com/example/android_hit/NetworkError.kt new file mode 100644 index 0000000000000000000000000000000000000000..faeceb8f15ff12b5053691bdf56386fb81e54473 --- /dev/null +++ b/app/src/main/java/com/example/android_hit/NetworkError.kt @@ -0,0 +1,59 @@ +package com.example.android_hit + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [NetworkError.newInstance] factory method to + * create an instance of this fragment. + */ +class NetworkError : Fragment() { + // TODO: Rename and change types of parameters + private var param1: String? = null + private var param2: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + param1 = it.getString(ARG_PARAM1) + param2 = it.getString(ARG_PARAM2) + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_network_error, container, false) + } + + companion object { + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param param1 Parameter 1. + * @param param2 Parameter 2. + * @return A new instance of fragment NetworkError. + */ + // TODO: Rename and change types and number of parameters + @JvmStatic + fun newInstance(param1: String, param2: String) = + NetworkError().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, param1) + putString(ARG_PARAM2, param2) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/NetworkManager.kt b/app/src/main/java/com/example/android_hit/NetworkManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..1d474c623dd9902b7dcfa222aa7aba404c714239 --- /dev/null +++ b/app/src/main/java/com/example/android_hit/NetworkManager.kt @@ -0,0 +1,71 @@ +package com.example.android_hit + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import androidx.lifecycle.LiveData + +class NetworkManager(context: Context) : LiveData<Boolean>() { +// private val _isNetworkAvailable = MutableLiveData<Boolean>() +// val isNetworkAvailable: LiveData<Boolean> get() = _isNetworkAvailable + + + + private var connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + // network is available for use + override fun onAvailable(network: Network) { + super.onAvailable(network) + postValue(true) + } + + override fun onUnavailable() { + super.onUnavailable() + postValue(false) + } + + // Network capabilities have changed for the network +// override fun onCapabilitiesChanged( +// network: Network, +// networkCapabilities: NetworkCapabilities +// ) { +// super.onCapabilitiesChanged(network, networkCapabilities) +// val unmetered = +// networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) +// } + + // lost network connection + override fun onLost(network: Network) { + super.onLost(network) + postValue(false) + } + } + private fun checkNetworkConnectivity() { + val network = connectivityManager.activeNetwork + if(network == null){ + postValue(false) + } + val requestBuilder = NetworkRequest.Builder().apply { + addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + }.build() + connectivityManager.registerNetworkCallback(requestBuilder, networkCallback) + } + + override fun onActive() { + super.onActive() + checkNetworkConnectivity() + } + + override fun onInactive() { + super.onInactive() + connectivityManager.unregisterNetworkCallback(networkCallback) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/Scan.kt b/app/src/main/java/com/example/android_hit/Scan.kt index faa0d42b9d84f915ce8f866c5d6005eabfd7977f..addd17e967a5fadd8743102c2312c5ab64aa5356 100644 --- a/app/src/main/java/com/example/android_hit/Scan.kt +++ b/app/src/main/java/com/example/android_hit/Scan.kt @@ -2,6 +2,8 @@ package com.example.android_hit import android.Manifest import android.app.Activity +import android.app.AlertDialog +import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -9,34 +11,43 @@ import android.graphics.ImageDecoder import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.provider.MediaStore +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView +import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import com.example.android_hit.api.RetrofitClient +import com.example.android_hit.data.ScanResponse +import com.example.android_hit.room.TransactionDB +import com.example.android_hit.room.TransactionEntity +import com.example.android_hit.utils.TokenManager +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.toRequestBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [Scan.newInstance] factory method to - * create an instance of this fragment. - */ class Scan : Fragment() { - // TODO: Rename and change types of parameters var pickedPhoto : Uri? = null var pickedBitMap : Bitmap? = null private val CAMERA_REQUEST_CODE = 1 private lateinit var btnCapture : Button private lateinit var btnPick : Button private lateinit var ivPicture : ImageView + private lateinit var sharedPref : TokenManager + @@ -52,8 +63,6 @@ class Scan : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_scan, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -62,7 +71,7 @@ class Scan : Fragment() { ivPicture = view.findViewById(R.id.captureImageView) btnPick = view.findViewById(R.id.pickImgBtn) btnCapture.isEnabled = true - + sharedPref = TokenManager(requireContext()) if(ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.CAMERA), @@ -86,6 +95,13 @@ class Scan : Fragment() { if(requestCode == 101){ var pic : Bitmap? = data?.getParcelableExtra<Bitmap>("data") ivPicture.setImageBitmap(pic) + pickedBitMap = pic + val savedImageUri = saveImageToInternalStorage(pickedBitMap!!) + pickedPhoto = savedImageUri + Handler(Looper.getMainLooper()).postDelayed({ + showConfirmationDialog() + }, 700) + } if(requestCode == 2 && resultCode == Activity.RESULT_OK && data != null){ pickedPhoto = data.data @@ -93,14 +109,39 @@ class Scan : Fragment() { val source = ImageDecoder.createSource(requireContext().contentResolver, pickedPhoto!!) pickedBitMap = ImageDecoder.decodeBitmap(source) ivPicture.setImageBitmap(pickedBitMap) + Handler(Looper.getMainLooper()).postDelayed({ + showConfirmationDialog() + }, 700) }else{ pickedBitMap = MediaStore.Images.Media.getBitmap(requireContext().contentResolver, pickedPhoto) ivPicture.setImageBitmap(pickedBitMap) + Handler(Looper.getMainLooper()).postDelayed({ + showConfirmationDialog() + }, 700) + } } } + + private fun saveImageToInternalStorage(bitmap: Bitmap?): Uri? { + // Check for null + if (bitmap == null) { + return null + } + + // Save the bitmap to a file + val filename = "${System.currentTimeMillis()}.jpg" + val file = File(requireContext().externalCacheDir, filename) + val fileOutputStream = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream) + fileOutputStream.close() + + // Get the Uri of the file + return Uri.fromFile(file) + } + override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, @@ -127,24 +168,79 @@ class Scan : Fragment() { startActivityForResult(galleryIntext, 2) } } - companion object { - /** - * Use this factory method to create a new instance of - * this fragment using the provided parameters. - * - * @param param1 Parameter 1. - * @param param2 Parameter 2. - * @return A new instance of fragment Scan. - */ - - // TODO: Rename and change types and number of parameters - @JvmStatic - fun newInstance(param1: String, param2: String) = - Scan().apply { - arguments = Bundle().apply { - putString(ARG_PARAM1, param1) - putString(ARG_PARAM2, param2) - } + private fun showConfirmationDialog() { + val alertDialogBuilder = AlertDialog.Builder(this.context) + alertDialogBuilder.apply { + setTitle("Confirmation") + setMessage("Are you sure to use this image?") + setPositiveButton("Yes") { dialogInterface: DialogInterface, _: Int -> + + + dialogInterface.dismiss() + + + // Ubah Bitmap menjadi ByteArrayOutputStream + val byteArrayOutputStream = ByteArrayOutputStream() + pickedBitMap?.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) + // Ubah ByteArrayOutputStream menjadi byte array + val byteArray = byteArrayOutputStream.toByteArray() + + // Ubah byte array menjadi RequestBody + val requestBody = byteArray.toRequestBody("image/jpg".toMediaTypeOrNull()) + + // Gunakan requestBody ini untuk mengirim gambar melalui Retrofit + val token = sharedPref.getToken() + val file = File(pickedPhoto?.path) + val filePart = MultipartBody.Part.createFormData("file", file.name, requestBody) + val call = RetrofitClient.apiService.uploadNota("Bearer $token", filePart) + call.enqueue(object : Callback<ScanResponse> { + override fun onResponse(call: Call<ScanResponse>, response: Response<ScanResponse>) { + if (response.isSuccessful) { + val responseBody = response.body() + Log.e("POST Success", "Response: ${responseBody.toString()}") + Toast.makeText(requireContext(), "Upload Success", Toast.LENGTH_SHORT).show() + + // Get the items from the response + val items = responseBody?.items?.items + + // Get a reference to the database + val db = TransactionDB.getInstance(requireContext()) + val transactionDao = db.transactionDao + + // Loop through the items and convert them to transactions + items?.forEach { item -> + val amount = item.qty * item.price + val transaction = TransactionEntity( + title = item.name, + amount = amount.toInt(), + category = "Expense", + location = "Location", // Replace with actual location + timestamp = System.currentTimeMillis().toString() // Replace with actual timestamp + ) + + // Insert the transaction into the database + transactionDao.addTransaction(transaction) + } + + } else { + Log.e("POST Error", "Failed to make POST request: ${response.message()}") + Toast.makeText(requireContext(), "Upload Failed: ${response.message()}", Toast.LENGTH_SHORT).show() + } + } + override fun onFailure(call: Call<ScanResponse>, t: Throwable) { + Log.e("POST Error", "Failed to make POST request: ${t.message}") + } + }) } + setNegativeButton("No") { dialogInterface: DialogInterface, _: Int -> + dialogInterface.dismiss() + ivPicture.setImageBitmap(null) + } + setCancelable(false) + } + + val alertDialog = alertDialogBuilder.create() + alertDialog.show() } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/Settings.kt b/app/src/main/java/com/example/android_hit/Settings.kt index 338766e7e1f66812a27244f75255f6db96bd8f27..cd1fb776c1030aef912f2ee20f5e5b43431e081c 100644 --- a/app/src/main/java/com/example/android_hit/Settings.kt +++ b/app/src/main/java/com/example/android_hit/Settings.kt @@ -1,46 +1,45 @@ package com.example.android_hit +import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog import android.content.DialogInterface import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle +import android.os.Environment import android.util.Log -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.TextView +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.fragment.app.Fragment +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.example.android_hit.room.TransactionDB +import com.example.android_hit.room.TransactionEntity import com.example.android_hit.utils.TokenManager import com.example.android_hit.utils.UserManager +import org.apache.poi.hssf.usermodel.HSSFWorkbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.File +import java.io.FileOutputStream +import kotlin.random.Random -// TODO: Rename parameter arguments, choose names that match -// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER -private const val ARG_PARAM1 = "param1" -private const val ARG_PARAM2 = "param2" - -/** - * A simple [Fragment] subclass. - * Use the [Settings.newInstance] factory method to - * create an instance of this fragment. - */ class Settings : Fragment() { - // TODO: Rename and change types of parameters - private var param1: String? = null - private var param2: String? = null + private lateinit var sharedPref : TokenManager private lateinit var logoutButton : Button + private lateinit var randomize : Button private lateinit var emailTextView : TextView private lateinit var user: UserManager - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - arguments?.let { - param1 = it.getString(ARG_PARAM1) - param2 = it.getString(ARG_PARAM2) - } - } + private val RANDOMIZE_ACTION = "com.example.android_hit.RANDOMIZE_ACTION" + private lateinit var saveButton: Button + private lateinit var sendButton: Button @SuppressLint("UseRequireInsteadOfGet", "MissingInflatedId") override fun onCreateView( @@ -53,14 +52,98 @@ class Settings : Fragment() { user = this.context?.let { UserManager(it) }!! logoutButton = view.findViewById(R.id.logoutButton) emailTextView = view.findViewById(R.id.emailTextView) - + randomize = view.findViewById(R.id.randomizeTransactionButton) + saveButton = view.findViewById(R.id.saveTransactionButton) + sendButton = view.findViewById(R.id.sendTransactionButton) emailTextView.text = user.getEmail("EMAIL") - Log.e("SET","masuk sini") - + val database = TransactionDB.getInstance(requireContext()) + val transactionDao = database.transactionDao + val transactions = transactionDao.getAllTransaction() logoutButton.setOnClickListener { showConfirmationDialog() + } + + randomize.setOnClickListener { + goToTransaction() + Log.e("BROD","clicked randomize") + val randomIntInRange = Random.nextInt(1000, 100000) + val intent = Intent(RANDOMIZE_ACTION) + intent.putExtra("hargaRandom",randomIntInRange.toString()) + LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent) + + } + saveButton.setOnClickListener { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0) + } + val fileFormats = arrayOf("xls", "xlsx") + + AlertDialog.Builder(requireContext()).apply { + setTitle("Choose file format") + setItems(fileFormats) { dialog, which -> + val fileFormat = fileFormats[which] + val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + val documentsFolder = File(documentsDir, "Bondoman-Transaction") + if (!documentsFolder.exists()) { + documentsFolder.mkdirs() + } + val title = "transactions.$fileFormat" + val file = File(documentsFolder, title) + val fileOutputStream = FileOutputStream(file) + saveTransactionsToExcel(transactions, fileFormat, fileOutputStream) + fileOutputStream.close() + Toast.makeText(requireContext(), "Transaksi berhasil disimpan", Toast.LENGTH_SHORT).show() + } + setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + }.create().show() + } + sendButton.setOnClickListener { + val fileFormats = arrayOf("xls", "xlsx") + + AlertDialog.Builder(requireContext()).apply { + setTitle("Choose file format") + setItems(fileFormats) { dialog, which -> + val fileFormat = fileFormats[which] + val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + val documentsFolder = File(documentsDir, "Bondoman-Transaction") + if (!documentsFolder.exists()) { + documentsFolder.mkdirs() + } + val title = "transactions.$fileFormat" + val file = File(documentsFolder, title) + val fileOutputStream = FileOutputStream(file) + Log.e("SET","fileFormat $fileFormat") + saveTransactionsToExcel(transactions, fileFormat, fileOutputStream) + fileOutputStream.close() + + + val uri = FileProvider.getUriForFile(requireContext(), "com.example.android_hit.fileprovider", file) + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "*/*" + putExtra(Intent.EXTRA_EMAIL, arrayOf(user.getEmail("EMAIL"))) + putExtra(Intent.EXTRA_SUBJECT, "Daftar Transaksi") + putExtra(Intent.EXTRA_TEXT, "Berikut adalah daftar transaksi Anda.") + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + Log.e("email","${user.getEmail("EMAIL")}") + + if (emailIntent.resolveActivity(requireContext().packageManager) != null) { + startActivity(emailIntent) + + } else { + Toast.makeText(requireContext(), "Tidak ada aplikasi email yang dapat menangani permintaan ini.", Toast.LENGTH_SHORT).show() + } + } + setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + }.create().show() } + return view } private fun showConfirmationDialog() { @@ -91,4 +174,47 @@ class Settings : Fragment() { startActivity(intent) } + private fun goToTransaction(){ + val intent = Intent(activity, DetailTransactionActivity::class.java) + startActivity(intent) + } + + + + private fun saveTransactionsToExcel(transactions: List<TransactionEntity>, fileFormat:String, outputStream: FileOutputStream) { + Log.e("SET","fileFormat $fileFormat") + + val workbook = if (fileFormat == "xls") { + HSSFWorkbook() + } else { + XSSFWorkbook() + } + + val sheet = workbook.createSheet("Transactions") + + + val headerRow = sheet.createRow(0) + headerRow.createCell(0).setCellValue("ID") + headerRow.createCell(1).setCellValue("Title") + headerRow.createCell(2).setCellValue("Amount") + headerRow.createCell(3).setCellValue("Category") + headerRow.createCell(4).setCellValue("Location") + headerRow.createCell(5).setCellValue("Timestamp") + + transactions.forEachIndexed { index, transaction -> + val row = sheet.createRow(index + 1) + row.createCell(0).setCellValue(transaction.id?.toDouble() ?: 0.0) + row.createCell(1).setCellValue(transaction.title) + row.createCell(2).setCellValue(transaction.amount.toDouble()) + row.createCell(3).setCellValue(transaction.category) + row.createCell(4).setCellValue(transaction.location) + row.createCell(5).setCellValue(transaction.timestamp) + } + // Menyimpan workbook ke file + workbook.write(outputStream) + workbook.close() + + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/api/ApiService.kt b/app/src/main/java/com/example/android_hit/api/ApiService.kt index b5a69afdc173f3c83f0166289b1b6d307341cee3..8933b30ee2d9c8dfef7df28ca119b1e88fe8643d 100644 --- a/app/src/main/java/com/example/android_hit/api/ApiService.kt +++ b/app/src/main/java/com/example/android_hit/api/ApiService.kt @@ -2,12 +2,15 @@ package com.example.android_hit.api import com.example.android_hit.data.LoginPayload import com.example.android_hit.data.LoginResponse +import com.example.android_hit.data.ScanResponse import com.example.android_hit.data.TokenResponse +import okhttp3.MultipartBody import retrofit2.Call import retrofit2.http.Body import retrofit2.http.Header -import retrofit2.http.Headers +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part interface ApiService { @@ -18,4 +21,9 @@ interface ApiService { @POST("/api/auth/token") fun cekToken(@Header("Authorization") token: String): Call<TokenResponse> + @Multipart + @POST("/api/bill/upload") + fun uploadNota(@Header("Authorization") token: String, + @Part file: MultipartBody.Part): Call<ScanResponse> + } \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/data/ScanResponse.kt b/app/src/main/java/com/example/android_hit/data/ScanResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..94b1062b4e4aeb855cddc8100cfa7fcf395b5f89 --- /dev/null +++ b/app/src/main/java/com/example/android_hit/data/ScanResponse.kt @@ -0,0 +1,13 @@ +package com.example.android_hit.data + +data class ScanResponse( + val items : Items +) +data class Items( + val items : List<Item> +) +data class Item( + val name : String, + val qty : Int, + val price : Double +) \ No newline at end of file diff --git a/app/src/main/java/com/example/android_hit/utils/TokenManager.kt b/app/src/main/java/com/example/android_hit/utils/TokenManager.kt index 7eb358d9745d69d296cb8fe60be588e80ffb52bd..461be58aa598d625c4f4d334d0f11ba89955a851 100644 --- a/app/src/main/java/com/example/android_hit/utils/TokenManager.kt +++ b/app/src/main/java/com/example/android_hit/utils/TokenManager.kt @@ -56,4 +56,9 @@ class TokenManager( context: Context) { fun deleteToken(){ editor.clear().apply() } + + fun getToken(): String? { + + return sharedPref.getString("TOKEN", null) + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/iv_network_check.xml b/app/src/main/res/drawable/iv_network_check.xml new file mode 100644 index 0000000000000000000000000000000000000000..dab031b17951ddffb35e3f5d8d90bb9e2b81dcde --- /dev/null +++ b/app/src/main/res/drawable/iv_network_check.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#322F50" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M15.9,5c-0.17,0 -0.32,0.09 -0.41,0.23l-0.07,0.15 -5.18,11.65c-0.16,0.29 -0.26,0.61 -0.26,0.96 0,1.11 0.9,2.01 2.01,2.01 0.96,0 1.77,-0.68 1.96,-1.59l0.01,-0.03L16.4,5.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM1,9l2,2c2.88,-2.88 6.79,-4.08 10.53,-3.62l1.19,-2.68C9.89,3.84 4.74,5.27 1,9zM21,11l2,-2c-1.64,-1.64 -3.55,-2.82 -5.59,-3.57l-0.53,2.82c1.5,0.62 2.9,1.53 4.12,2.75zM17,15l2,-2c-0.8,-0.8 -1.7,-1.42 -2.66,-1.89l-0.55,2.92c0.42,0.27 0.83,0.59 1.21,0.97zM5,13l2,2c1.13,-1.13 2.56,-1.79 4.03,-2l1.28,-2.88c-2.63,-0.08 -5.3,0.87 -7.31,2.88z"/> +</vector> diff --git a/app/src/main/res/layout/fragment_graphs.xml b/app/src/main/res/layout/fragment_graphs.xml index c4a97b48b38fa2f5c75b09d4446e5adf8e2387aa..e2436c8874fdd9a59b1f61a8824f17294c2305dd 100644 --- a/app/src/main/res/layout/fragment_graphs.xml +++ b/app/src/main/res/layout/fragment_graphs.xml @@ -11,11 +11,11 @@ android:layout_height="match_parent"> - <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="8dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -114,7 +114,9 @@ <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="match_parent"/> + android:layout_height="match_parent"> + + </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/app/src/main/res/layout/fragment_header_network.xml b/app/src/main/res/layout/fragment_header_network.xml new file mode 100644 index 0000000000000000000000000000000000000000..8658c4eb6453f58e35e187e953ff3448e42e4dcf --- /dev/null +++ b/app/src/main/res/layout/fragment_header_network.xml @@ -0,0 +1,8 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/primary_color_4" + > +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_network_error.xml b/app/src/main/res/layout/fragment_network_error.xml new file mode 100644 index 0000000000000000000000000000000000000000..130fe170d4cca4a32cc04993696af16542824426 --- /dev/null +++ b/app/src/main/res/layout/fragment_network_error.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout 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" + tools:context=".NetworkError"> + + <!-- TODO: Update blank fragment layout --> + <ImageView + android:id="@+id/iv_network" + android:layout_width="91dp" + android:layout_height="129dp" + android:layout_centerInParent="true" + android:src="@drawable/iv_network_check" /> + + <TextView + android:id="@+id/text_network_check" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/iv_network" + android:layout_alignParentEnd="true" + android:layout_marginTop="13dp" + android:layout_marginEnd="164dp" + android:layout_marginBottom="451dp" + android:text="@string/network_check" /> + + <TextView + android:id="@+id/text_network_check_desc" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/iv_network" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_marginTop="47dp" + android:layout_marginEnd="82dp" + android:layout_marginBottom="235dp" + android:text="@string/network_check_desc" /> + +</RelativeLayout> \ 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 f734c21dae73c35315e00db5756cee63f760e3a4..637c6d10d816c815effe64b0f4b0af83cd7ce384 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -50,6 +50,15 @@ android:text="Send Transaction List" android:layout_marginVertical="10dp" android:textColor="@color/primary1" /> + <Button + android:id="@+id/randomizeTransactionButton" + android:layout_marginHorizontal="16dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/rounded_background_transparent" + android:text="Randomize" + android:layout_marginVertical="10dp" + android:textColor="@color/primary1" /> <Button android:id="@+id/logoutButton" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e42b46d21fe46ae7e868af63732409e79893de44..16a6ea38e2a78a35ce190a4ab139c30c29cf2d01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ -<resources> +<resources xmlns:tools="http://schemas.android.com/tools"> <string name="app_name">Android-HIT</string> <string name="tagline_awal">One Touch Solution for Your Budgeting</string> <string name="desc_tagline_awal">Overbudget? Don’t worry! With us, managing your income and expanses is only one touch away</string> @@ -7,7 +7,6 @@ <string name="greet">Hi!</string> <string name="password">Password </string> <string name="email">Email</string> - <!-- TODO: Remove or change this placeholder text --> <string name="hello_blank_fragment">Hello blank fragment</string> <!-- Strings related to login --> <string name="prompt_email">Email</string> @@ -18,10 +17,14 @@ <string name="invalid_username">Not a valid username</string> <string name="invalid_password">Password must be >5 characters</string> <string name="login_failed">"Login failed"</string> - <!-- TODO: Remove or change this placeholder text --> + <string name="fragment_transaction">Transaction</string> <string name="fragment_settings">Settings</string> <string name="fragment_graphs">Graphs</string> <string name="fragment_scan">Scan</string> + <string name="network_check">Network Error</string> + <string name="network_check_desc">Please check your network connection</string> + + </resources> \ No newline at end of file