add support to view previous transfer recipts

This commit is contained in:
2026-05-19 19:39:20 +05:00
parent 28e5878668
commit b24949c117
8 changed files with 356 additions and 7 deletions
@@ -0,0 +1,108 @@
package sh.sar.basedbank.ui.home
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.databinding.ItemDateHeaderBinding
import sh.sar.basedbank.databinding.ItemTransactionBinding
import sh.sar.basedbank.util.ReceiptStore
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class ActivitiesAdapter(
private val onItemClick: (ReceiptStore.Entry) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private sealed class Item {
data class DateHeader(val label: String) : Item()
data class ReceiptItem(val entry: ReceiptStore.Entry) : Item()
}
private val displayItems = mutableListOf<Item>()
fun setEntries(entries: List<ReceiptStore.Entry>) {
displayItems.clear()
var lastDateKey = ""
for (entry in entries) {
val dateKey = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date(entry.savedAt))
if (dateKey != lastDateKey) {
displayItems.add(Item.DateHeader(formatDateHeader(entry.savedAt)))
lastDateKey = dateKey
}
displayItems.add(Item.ReceiptItem(entry))
}
notifyDataSetChanged()
}
override fun getItemCount() = displayItems.size
override fun getItemViewType(position: Int) =
if (displayItems[position] is Item.DateHeader) TYPE_DATE_HEADER else TYPE_RECEIPT
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return if (viewType == TYPE_DATE_HEADER)
DateHeaderVH(ItemDateHeaderBinding.inflate(inflater, parent, false))
else
ReceiptVH(ItemTransactionBinding.inflate(inflater, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is DateHeaderVH -> holder.bind((displayItems[position] as Item.DateHeader).label)
is ReceiptVH -> holder.bind((displayItems[position] as Item.ReceiptItem).entry)
}
}
inner class DateHeaderVH(private val b: ItemDateHeaderBinding) :
RecyclerView.ViewHolder(b.root) {
fun bind(label: String) { b.tvDateHeader.text = label }
}
inner class ReceiptVH(private val b: ItemTransactionBinding) :
RecyclerView.ViewHolder(b.root) {
fun bind(entry: ReceiptStore.Entry) {
val d = entry.data
val colorHex = d.fromColorHex.takeIf { it.isNotBlank() } ?: "#607D8B"
val initial = d.toLabel.firstOrNull()?.uppercaseChar()?.toString() ?: "?"
b.fvAvatar.background = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(try { Color.parseColor(colorHex) } catch (_: Exception) { Color.GRAY })
}
b.tvInitial.visibility = android.view.View.VISIBLE
b.tvInitial.text = initial
b.tvCounterparty.text = d.toLabel
b.tvCounterparty.visibility = android.view.View.VISIBLE
b.tvDescription.text = buildString {
append(d.fromLabel)
if (d.toBank.isNotBlank()) append(" · ${d.toBank}")
}
b.tvDate.text = formatTime(entry.savedAt)
b.tvAmount.text = "- ${d.currency} ${d.amount}"
b.tvAmount.setTextColor(Color.parseColor("#FF7043"))
b.root.setOnClickListener { onItemClick(entry) }
}
}
private fun formatDateHeader(millis: Long): String {
val sdf = SimpleDateFormat("EEEE, d MMMM yyyy", Locale.US)
return sdf.format(Date(millis))
}
private fun formatTime(millis: Long): String {
val sdf = SimpleDateFormat("HH:mm", Locale.US)
return sdf.format(Date(millis))
}
companion object {
private const val TYPE_DATE_HEADER = 0
private const val TYPE_RECEIPT = 1
}
}
@@ -0,0 +1,95 @@
package sh.sar.basedbank.ui.home
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentActivitiesBinding
import sh.sar.basedbank.util.ReceiptStore
class ActivitiesFragment : Fragment() {
private var _binding: FragmentActivitiesBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: ActivitiesAdapter
private val allEntries = mutableListOf<ReceiptStore.Entry>()
private var searchQuery = ""
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentActivitiesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = ActivitiesAdapter { entry ->
(activity as? HomeActivity)?.showWithBackStack(
TransferReceiptFragment.newInstance(entry.data, null)
)
}
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
val bottomPaddingBase = (16 * resources.displayMetrics.density).toInt()
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
val isBottomNav = requireContext()
.getSharedPreferences("prefs", Context.MODE_PRIVATE)
.getBoolean("bottom_nav", false)
val navBar = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val extraBottom = if (isBottomNav) 0 else navBar.bottom
v.setPadding(v.paddingLeft, v.paddingTop, v.paddingRight, bottomPaddingBase + extraBottom)
insets
}
binding.etSearch.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
searchQuery = s?.toString()?.trim() ?: ""
filterAndDisplay()
}
})
loadEntries()
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_activities)
// Reload in case a new receipt was added while we were away
loadEntries()
}
private fun loadEntries() {
allEntries.clear()
allEntries.addAll(ReceiptStore.loadAll(requireContext()))
filterAndDisplay()
}
private fun filterAndDisplay() {
val filtered = if (searchQuery.isBlank()) allEntries
else allEntries.filter { entry ->
entry.data.toLabel.contains(searchQuery, ignoreCase = true) ||
entry.data.fromLabel.contains(searchQuery, ignoreCase = true) ||
entry.data.toAccount.contains(searchQuery, ignoreCase = true) ||
entry.data.toBank.contains(searchQuery, ignoreCase = true) ||
entry.data.mibReferenceNo.contains(searchQuery, ignoreCase = true) ||
entry.data.bmlReference.contains(searchQuery, ignoreCase = true)
}
adapter.setEntries(filtered)
binding.emptyView.visibility = if (filtered.isEmpty()) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -116,6 +116,7 @@ class HomeActivity : AppCompatActivity() {
R.id.nav_contacts -> ContactsFragment()
R.id.nav_transfer -> TransferFragment()
R.id.nav_more -> MoreFragment()
R.id.nav_activities -> ActivitiesFragment()
R.id.nav_transfer_history -> TransferHistoryFragment()
R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment()
@@ -276,6 +277,7 @@ class HomeActivity : AppCompatActivity() {
R.id.nav_accounts -> AccountsFragment()
R.id.nav_contacts -> ContactsFragment()
R.id.nav_transfer -> TransferFragment()
R.id.nav_activities -> ActivitiesFragment()
R.id.nav_transfer_history -> TransferHistoryFragment()
R.id.nav_finances -> FinancingFragment()
R.id.nav_otp -> OtpFragment()
@@ -56,6 +56,7 @@ import sh.sar.basedbank.util.AccountInputParser
import sh.sar.basedbank.util.PaymvQrParser
import sh.sar.basedbank.util.RecentPick
import sh.sar.basedbank.util.RecentsCache
import sh.sar.basedbank.util.ReceiptStore
import sh.sar.basedbank.util.Totp
class TransferFragment : Fragment() {
@@ -628,6 +629,7 @@ class TransferFragment : Fragment() {
binding.btnTransfer.isEnabled = true
(activity as? HomeActivity)?.setRefreshing(false)
if (ok && receipt != null) {
ReceiptStore.save(requireContext(), receipt)
clearForm()
val activity = requireActivity() as HomeActivity
activity.refreshBalances(src)
@@ -755,7 +757,7 @@ class TransferFragment : Fragment() {
)
if (result.success) {
val receipt = TransferReceiptData(
isMib = true,
bank = "MIB",
amount = "%.2f".format(amount.toDoubleOrNull() ?: 0.0),
currency = currency,
fromLabel = src.accountBriefName,
@@ -839,7 +841,7 @@ class TransferFragment : Fragment() {
val result = bmlFlow.confirmTransfer(sess, debitAccount, creditAccount, amount, transferType, currency, confirmOtp, remarks, bank)
if (result.success) {
val receipt = TransferReceiptData(
isMib = false,
bank = "BML",
amount = "%.2f".format(amount),
currency = currency,
fromLabel = src.accountBriefName,
@@ -1,7 +1,7 @@
package sh.sar.basedbank.ui.home
data class TransferReceiptData(
val isMib: Boolean,
val bank: String, // "MIB", "BML", etc.
val amount: String,
val currency: String,
val fromLabel: String,
@@ -46,7 +46,7 @@ class TransferReceiptFragment : Fragment() {
private val receiptCard get() = _receiptCard!!
companion object {
private const val ARG_IS_MIB = "is_mib"
private const val ARG_BANK = "bank"
private const val ARG_AMOUNT = "amount"
private const val ARG_CURRENCY = "currency"
private const val ARG_FROM_LABEL = "from_label"
@@ -69,7 +69,7 @@ class TransferReceiptFragment : Fragment() {
fun newInstance(data: TransferReceiptData, toAvatarBitmap: Bitmap?) = TransferReceiptFragment().apply {
pendingToAvatarBitmap = toAvatarBitmap
arguments = Bundle().apply {
putBoolean(ARG_IS_MIB, data.isMib)
putString(ARG_BANK, data.bank)
putString(ARG_AMOUNT, data.amount)
putString(ARG_CURRENCY, data.currency)
putString(ARG_FROM_LABEL, data.fromLabel)
@@ -90,8 +90,8 @@ class TransferReceiptFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val isMib = arguments?.getBoolean(ARG_IS_MIB, true) ?: true
return if (isMib) {
val bank = arguments?.getString(ARG_BANK, "MIB") ?: "MIB"
return if (bank == "MIB") {
val binding = FragmentReceiptMibBinding.inflate(inflater, container, false)
bindMib(binding)
_receiptCard = binding.receiptCard