add support to View details of blocked balance #33
Auto Tag on Version Change / check-version (push) Successful in 3s

This commit is contained in:
2026-06-03 23:12:02 +05:00
parent 3bb44f1c32
commit 0e5435f0fe
3 changed files with 103 additions and 13 deletions
@@ -153,6 +153,46 @@ class BmlHistoryClient {
} catch (_: Exception) { emptyList() }
}
fun fetchPendingHistory(
session: BmlSession,
accountId: String,
accountDisplayName: String,
accountNumber: String
): List<BankTransaction> {
val resp = client.newCall(
Request.Builder().url("$BML_BASE_URL/api/mobile/history/pending/$accountId")
.header("Authorization", "Bearer ${session.accessToken}")
.header("User-Agent", BML_USER_AGENT)
.header("x-app-version", BML_APP_VERSION)
.build()
).execute()
val code = resp.code
val json = resp.body?.string() ?: return emptyList()
resp.close()
if (code == 401 || code == 419) throw AuthExpiredException()
if (code in 500..599) throw BankServerException("BML")
return try {
val root = JSONObject(json)
if (!root.optBoolean("success")) return emptyList()
val payload = root.optJSONArray("payload") ?: return emptyList()
(0 until payload.length()).map { i ->
val item = payload.getJSONObject(i)
BankTransaction(
id = item.optString("LockedID"),
date = item.optString("FromDate"),
description = "Pending",
amount = -item.optDouble("LockedAmount", 0.0),
currency = "MVR",
counterpartyName = item.optString("Description").trim().takeIf { it.isNotBlank() },
reference = null,
accountNumber = accountNumber,
accountDisplayName = accountDisplayName,
source = "BML"
)
}
} catch (_: Exception) { emptyList() }
}
// "12-05-2026 041675" → first 4 digits of time part as HH:mm
private fun parsePurchaseNarrative1(narrative1: String): String? {
return try {
@@ -27,9 +27,10 @@ class AccountHistoryAdapter(
private sealed class Item {
data class DateHeader(val label: String) : Item()
data class Trx(val transaction: BankTransaction) : Item()
data class Trx(val transaction: BankTransaction, val showDate: Boolean = false) : Item()
}
private val pendingItems = mutableListOf<Item>()
private val displayItems = mutableListOf<Item>()
private var lastInsertedDateKey = ""
private val imageCache = mutableMapOf<String, Bitmap>()
@@ -48,9 +49,11 @@ class AccountHistoryAdapter(
if (hideAmounts == hide) return
hideAmounts = hide
notifyItemChanged(0) // refresh header card
// refresh all transaction rows
for (i in pendingItems.indices) {
if (pendingItems[i] is Item.Trx) notifyItemChanged(i + 1)
}
for (i in displayItems.indices) {
if (displayItems[i] is Item.Trx) notifyItemChanged(i + 1)
if (displayItems[i] is Item.Trx) notifyItemChanged(1 + pendingItems.size + i)
}
}
@@ -58,7 +61,7 @@ class AccountHistoryAdapter(
imageCache[counterpartyName] = bitmap
displayItems.forEachIndexed { i, item ->
if (item is Item.Trx && item.transaction.counterpartyName == counterpartyName)
notifyItemChanged(i + 1) // +1 for account header at position 0
notifyItemChanged(1 + pendingItems.size + i)
}
}
@@ -66,10 +69,19 @@ class AccountHistoryAdapter(
iconUrlCache[url] = bitmap
displayItems.forEachIndexed { i, item ->
if (item is Item.Trx && item.transaction.iconUrl == url)
notifyItemChanged(i + 1) // +1 for account header at position 0
notifyItemChanged(1 + pendingItems.size + i)
}
}
fun setPendingTransactions(transactions: List<BankTransaction>) {
pendingItems.clear()
if (transactions.isNotEmpty()) {
pendingItems.add(Item.DateHeader("Pending"))
for (trx in transactions) pendingItems.add(Item.Trx(trx, showDate = true))
}
notifyDataSetChanged()
}
private var _showLoadingFooter = false
var showLoadingFooter: Boolean
get() = _showLoadingFooter
@@ -127,18 +139,24 @@ class AccountHistoryAdapter(
displayItems.add(Item.Trx(trx))
}
val added = displayItems.size - oldCount
if (added > 0) notifyItemRangeInserted(1 + oldCount, added) // +1 for account header
if (added > 0) notifyItemRangeInserted(1 + pendingItems.size + oldCount, added)
}
// Position 0 = account header card
// Positions 1..displayItems.size = date headers + transactions
// Positions 1..pendingItems.size = pending header + pending transactions
// Positions 1+pendingItems.size..1+pendingItems.size+displayItems.size = date headers + transactions
// Last position = loading footer when showLoadingFooter = true
override fun getItemCount() = 1 + displayItems.size + if (_showLoadingFooter) 1 else 0
override fun getItemCount() = 1 + pendingItems.size + displayItems.size + if (_showLoadingFooter) 1 else 0
private fun itemAt(position: Int): Item {
val idx = position - 1
return if (idx < pendingItems.size) pendingItems[idx] else displayItems[idx - pendingItems.size]
}
override fun getItemViewType(position: Int) = when {
position == 0 -> TYPE_HEADER
_showLoadingFooter && position == itemCount - 1 -> TYPE_LOADING
else -> when (displayItems[position - 1]) {
else -> when (itemAt(position)) {
is Item.DateHeader -> TYPE_DATE_HEADER
is Item.Trx -> TYPE_TRANSACTION
}
@@ -157,8 +175,11 @@ class AccountHistoryAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderVH -> holder.bind(display)
is DateHeaderVH -> holder.bind((displayItems[position - 1] as Item.DateHeader).label)
is TransactionVH -> holder.bind((displayItems[position - 1] as Item.Trx).transaction)
is DateHeaderVH -> holder.bind((itemAt(position) as Item.DateHeader).label)
is TransactionVH -> {
val item = itemAt(position) as Item.Trx
holder.bind(item.transaction, item.showDate)
}
else -> Unit
}
}
@@ -203,7 +224,7 @@ class AccountHistoryAdapter(
inner class TransactionVH(private val b: ItemTransactionBinding) :
RecyclerView.ViewHolder(b.root) {
fun bind(trx: BankTransaction) {
fun bind(trx: BankTransaction, showDate: Boolean = false) {
val isCredit = trx.amount >= 0
val color = sourceColor(trx.source)
val name = trx.counterpartyName ?: trx.description
@@ -239,7 +260,7 @@ class AccountHistoryAdapter(
b.tvCounterparty.visibility = View.GONE
}
b.tvDate.text = formatTime(trx.date)
b.tvDate.text = if (showDate) formatDateOnly(trx.date) else formatTime(trx.date)
if (hideAmounts) {
b.tvAmount.text = "${trx.currency} ••••••"
@@ -286,6 +307,7 @@ class AccountHistoryAdapter(
private val MIB_FMT = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
private val BML_FMT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US)
private val DATE_HEADER_FMT = SimpleDateFormat("MMMM d, yyyy", Locale.getDefault())
private val DATE_ONLY_FMT = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
private val TIME_FMT = SimpleDateFormat("h:mm a", Locale.getDefault())
private val FULL_DATE_FMT = SimpleDateFormat("MMM d, yyyy · h:mm a", Locale.getDefault())
@@ -307,6 +329,11 @@ class AccountHistoryAdapter(
return DATE_HEADER_FMT.format(date)
}
fun formatDateOnly(raw: String): String {
val date = parseDate(raw) ?: return raw.take(10)
return DATE_ONLY_FMT.format(date)
}
fun formatTime(raw: String): String {
val date = parseDate(raw) ?: return ""
return TIME_FMT.format(date)
@@ -24,6 +24,7 @@ import sh.sar.basedbank.BasedBankApp
import sh.sar.basedbank.R
import sh.sar.basedbank.api.models.BankAccount
import sh.sar.basedbank.api.models.BankServerException
import sh.sar.basedbank.api.bml.BmlHistoryClient
import sh.sar.basedbank.api.mib.MibContactsClient
import sh.sar.basedbank.api.models.BankTransaction
import sh.sar.basedbank.api.mib.TransactionCache
@@ -138,6 +139,7 @@ class AccountHistoryFragment : Fragment() {
}
(activity as? HomeActivity)?.setRefreshing(true)
loadNextPage()
loadPendingTransactions()
binding.swipeRefresh.setOnRefreshListener {
if (isLoading) {
@@ -184,6 +186,7 @@ class AccountHistoryFragment : Fragment() {
binding.emptyView.visibility = View.GONE
}
loadNextPage()
loadPendingTransactions()
}
private fun loadNextPage() {
@@ -250,6 +253,26 @@ class AccountHistoryFragment : Fragment() {
}
}
private fun loadPendingTransactions() {
if (account.bank != "BML" || account.profileType != "BML") return
val app = requireActivity().application as BasedBankApp
val session = app.bmlSessionFor(account) ?: return
viewLifecycleOwner.lifecycleScope.launch {
try {
val pending = withContext(Dispatchers.IO) {
BmlHistoryClient().fetchPendingHistory(
session = session,
accountId = account.internalId,
accountDisplayName = account.accountBriefName,
accountNumber = account.accountNumber
)
}
if (_binding == null) return@launch
adapter.setPendingTransactions(pending)
} catch (_: Exception) { }
}
}
private fun loadContactImage(name: String) {
if (!pendingImageNames.add(name)) return
val contact = viewModel.contacts.value?.firstOrNull { it.benefNickName == name } ?: return