From 0e5435f0fea0731805d8d016f924253ee5d8ee51 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Wed, 3 Jun 2026 23:12:02 +0500 Subject: [PATCH] add support to View details of blocked balance #33 --- .../sar/basedbank/api/bml/BmlHistoryClient.kt | 40 ++++++++++++++ .../ui/home/AccountHistoryAdapter.kt | 53 ++++++++++++++----- .../ui/home/AccountHistoryFragment.kt | 23 ++++++++ 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt b/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt index b33e747..23c3e71 100644 --- a/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt +++ b/app/src/main/java/sh/sar/basedbank/api/bml/BmlHistoryClient.kt @@ -153,6 +153,46 @@ class BmlHistoryClient { } catch (_: Exception) { emptyList() } } + fun fetchPendingHistory( + session: BmlSession, + accountId: String, + accountDisplayName: String, + accountNumber: String + ): List { + 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 { diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt index 24b3827..1303537 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryAdapter.kt @@ -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() private val displayItems = mutableListOf() private var lastInsertedDateKey = "" private val imageCache = mutableMapOf() @@ -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) { + 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) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt index 7d2f2ba..5700f52 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/AccountHistoryFragment.kt @@ -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