Compare commits

...

3 Commits

Author SHA1 Message Date
ahusan 8d09e760a8 Enhance dashboard: add attention row for blocked and overdue funds
Introduces a new attention row in the dashboard to display blocked funds and overdue financing. The row is conditionally visible based on the presence of blocked amounts or overdue totals. Updates the account display logic to show blocked amounts where applicable, ensuring users have a clear view of their financial status. Additionally, new string resources for "Blocked Funds" and "Overdue Financing" are added for localization.
2026-05-28 15:24:49 +05:00
ahusan 62ccae602d accounts list: use available balance, show blocked amount as secondary line
BML CASA rows on the accounts list were showing currentBalance (the
working/ledger balance, which includes blocked funds). Every other
balance display in the app — transfer screen, contact picker, QR pay,
dashboard totals — uses availableBalance, so the same account was
showing a different figure depending on where you looked at it.

This switches the accounts list to availableBalance for consistency,
and adds a small muted "MVR X.XX blocked" line beneath the balance
when blocked > 0 so the blocked funds are still visible at a glance.
Only BML reports a non-zero blocked amount; MIB and Fahipay rows are
unaffected.

The per-account history page header is untouched — its three-column
Available / Balance / Blocked breakdown still works as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:54:33 +05:00
ahusan 9011ef2f5a debug builds: separate applicationId so they coexist with release
Adds applicationIdSuffix=.debug and versionNameSuffix=-debug so a
side-loaded debug build can be installed alongside the Play/release
build without conflicting on package id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:53:45 +05:00
11 changed files with 229 additions and 26 deletions
+4
View File
@@ -27,6 +27,10 @@ android {
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
@@ -9,6 +9,7 @@ import org.json.JSONObject
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.abs
import kotlin.random.Random
class SessionExpiredException : Exception("MIB session expired")
@@ -168,7 +169,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"),
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage,
@@ -188,6 +189,13 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
// ─── Helpers ─────────────────────────────────────────────────────────────
/** MIB returns blockedAmount as a signed decimal where negative = funds held.
* Normalize to a positive magnitude so downstream code can treat it uniformly. */
private fun absBlockedAmount(raw: String): String {
val v = raw.toDoubleOrNull() ?: return raw
return "%.2f".format(abs(v))
}
private fun initialKeyExchange(
appId: String, encKey: String, sfunc: String, key2: String? = null
): Pair<MibSession, String> {
@@ -325,7 +333,7 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
accountTypeName = a.optString("accountTypeName"),
availableBalance = a.optString("availableBalance"),
currentBalance = a.optString("currentBalance"),
blockedAmount = a.optString("blockedAmount"),
blockedAmount = absBlockedAmount(a.optString("blockedAmount")),
mvrBalance = a.optString("mvrBalance"),
statusDesc = a.optString("statusDesc"),
profileImageHash = profile.customerImage,
@@ -125,6 +125,14 @@ class AccountsAdapter(
binding.tvAccountNumber.text = display.number
binding.tvAccountType.text = display.typeLabel
binding.tvBalance.text = if (hideAmounts) maskAmount(display.balance) else display.balance
val blocked = display.blockedBalance
if (blocked != null) {
val shown = if (hideAmounts) maskAmount(blocked) else blocked
binding.tvBlocked.text = binding.root.context.getString(R.string.account_blocked_label, shown)
binding.tvBlocked.visibility = View.VISIBLE
} else {
binding.tvBlocked.visibility = View.GONE
}
binding.btnTransfer.setOnClickListener { onTransferClick?.invoke(account) }
binding.root.setOnClickListener { onAccountClick(account) }
binding.root.setOnLongClickListener {
@@ -57,14 +57,24 @@ class DashboardFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() }
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() }
viewModel.accounts.observe(viewLifecycleOwner) {
updateBalances(it)
updateAttentionRow()
}
viewModel.financing.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) {
updatePendingFinances()
updateAttentionRow()
}
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
viewModel.hideAmounts.observe(viewLifecycleOwner) {
updateBalances(viewModel.accounts.value ?: emptyList())
updatePendingFinances()
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
updateAttentionRow()
}
binding.swipeRefresh.setOnRefreshListener {
@@ -76,6 +86,10 @@ class DashboardFragment : Fragment() {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
}
binding.cardOverdue.setOnClickListener {
(activity as? HomeActivity)?.navigateTo(R.id.nav_finances)
}
val cardAdapter = DashboardCardAdapter()
binding.rvCards.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.rvCards.adapter = cardAdapter
@@ -244,6 +258,56 @@ class DashboardFragment : Fragment() {
}
}
private fun updateAttentionRow() {
val hide = viewModel.hideAmounts.value ?: false
val accounts = viewModel.accounts.value ?: emptyList()
// Blocked: sum across CASA-style accounts (exclude cards and loans) per currency.
val blockedByCurrency = accounts
.filter { it.profileType != "BML_CREDIT" && it.profileType != "BML_PREPAID" && it.profileType != "BML_DEBIT" && it.profileType != "BML_LOAN" }
.mapNotNull { acc ->
val v = acc.blockedAmount.replace(",", "").toDoubleOrNull() ?: 0.0
if (v > 0.0) acc.currencyName.uppercase() to v else null
}
.groupBy({ it.first }, { it.second })
.mapValues { (_, vs) -> vs.sum() }
val blockedTotal = blockedByCurrency.values.sum()
if (blockedTotal > 0.0) {
// Primary line: prefer MVR if present, otherwise the first currency.
val primaryCcy = if ("MVR" in blockedByCurrency) "MVR" else blockedByCurrency.keys.first()
val primaryAmt = blockedByCurrency.getValue(primaryCcy)
binding.tvBlockedTotal.text = if (hide) "$primaryCcy ••••••" else "$primaryCcy %,.2f".format(primaryAmt)
val secondary = blockedByCurrency.filterKeys { it != primaryCcy }
if (secondary.isNotEmpty()) {
binding.tvBlockedSecondary.text = secondary.entries.joinToString(" · ") { (ccy, amt) ->
if (hide) "$ccy ••••••" else "$ccy %,.2f".format(amt)
}
binding.tvBlockedSecondary.visibility = View.VISIBLE
} else {
binding.tvBlockedSecondary.visibility = View.GONE
}
binding.cardBlocked.visibility = View.VISIBLE
} else {
binding.cardBlocked.visibility = View.GONE
}
// Overdue: MIB finance deals + BML loan details (assumed MVR — matches existing Pending Finances).
val mibOverdue = (viewModel.financing.value ?: emptyList()).sumOf { it.overdueAmount }
val bmlOverdue = (viewModel.bmlLoanDetails.value ?: emptyMap()).values.sumOf { it.overdueAmount }
val overdueTotal = mibOverdue + bmlOverdue
if (overdueTotal > 0.0) {
binding.tvOverdueTotal.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(overdueTotal)
binding.cardOverdue.visibility = View.VISIBLE
} else {
binding.cardOverdue.visibility = View.GONE
}
binding.rowAttention.visibility =
if (blockedTotal > 0.0 || overdueTotal > 0.0) View.VISIBLE else View.GONE
}
private fun updatePendingFinances() {
val hide = viewModel.hideAmounts.value ?: false
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
@@ -5,6 +5,7 @@ data class AccountListDisplay(
val number: String,
val typeLabel: String,
val balance: String,
val blockedBalance: String? = null, // null when zero or not applicable
val isCard: Boolean = false,
val cardBrandIcon: Int = 0, // drawable res, only meaningful if isCard
val statusLabel: String? = null // null = active; shown as status pill if set
@@ -26,11 +26,13 @@ object BmlDashboardParser {
statusLabel = if (isActive) null else account.statusDesc
)
} else {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
AccountListDisplay(
name = account.accountBriefName,
number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName),
balance = listBalance(account)
name = account.accountBriefName,
number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName),
balance = listBalance(account),
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
)
}
}
@@ -52,9 +54,9 @@ object BmlDashboardParser {
}
}
/** Balance shown in the accounts list — ledger (working) balance for BML CASA. */
/** Balance shown in the accounts list — available balance, consistent with transfer/contact picker. */
fun listBalance(account: BankAccount): String =
"${account.currencyName} ${account.currentBalance}"
"${account.currencyName} ${account.availableBalance}"
fun cardBrandIcon(productName: String): Int = when {
productName.contains("AMEX", ignoreCase = true) ||
@@ -5,12 +5,16 @@ import sh.sar.basedbank.util.AccountListDisplay
object MibAccountParser {
fun displayData(account: BankAccount) = AccountListDisplay(
name = account.accountBriefName,
number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName),
balance = "${account.currencyName} ${account.availableBalance}"
)
fun displayData(account: BankAccount): AccountListDisplay {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
return AccountListDisplay(
name = account.accountBriefName,
number = account.accountNumber,
typeLabel = productLabel(account.accountTypeName),
balance = "${account.currencyName} ${account.availableBalance}",
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
)
}
/**
* Returns a display-ready product label for a MIB (Faisanet) account type name.
@@ -5,13 +5,16 @@ import sh.sar.basedbank.util.AccountHistoryDisplay
object MibHistoryParser {
fun displayData(account: BankAccount) = AccountHistoryDisplay(
name = account.accountBriefName,
number = account.accountNumber,
bankPill = null, // MIB has no bank pill
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
availableBalance = "${account.currencyName} ${account.availableBalance}",
workingBalance = "${account.currencyName} ${account.currentBalance}",
blockedBalance = null
)
fun displayData(account: BankAccount): AccountHistoryDisplay {
val blocked = account.blockedAmount.toDoubleOrNull() ?: 0.0
return AccountHistoryDisplay(
name = account.accountBriefName,
number = account.accountNumber,
bankPill = null, // MIB has no bank pill
typeLabel = MibAccountParser.productLabel(account.accountTypeName),
availableBalance = "${account.currencyName} ${account.availableBalance}",
workingBalance = "${account.currencyName} ${account.currentBalance}",
blockedBalance = if (blocked > 0.0) "${account.currencyName} ${account.blockedAmount}" else null
)
}
}
@@ -176,6 +176,103 @@
</LinearLayout>
<!-- Attention row: Blocked + Overdue (hidden when nothing applies) -->
<LinearLayout
android:id="@+id/rowAttention"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp"
android:visibility="gone">
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardBlocked"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_blocked"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvBlockedSecondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer"
android:alpha="0.75"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardOverdue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
app:cardElevation="1dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorErrorContainer"
android:clickable="true"
android:focusable="true"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dashboard_overdue"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnErrorContainer" />
<TextView
android:id="@+id/tvOverdueTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="MVR —"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnErrorContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<!-- Pending Finances card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardPendingFinances"
+9
View File
@@ -72,6 +72,15 @@
android:textAppearance="?attr/textAppearanceTitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/tvBlocked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorError"
android:visibility="gone" />
<ImageButton
android:id="@+id/btnTransfer"
android:layout_width="40dp"
+3
View File
@@ -209,6 +209,9 @@
<string name="accounts">Accounts</string>
<string name="cards">Cards</string>
<string name="available_balance">Available Balance</string>
<string name="account_blocked_label">%1$s blocked</string>
<string name="dashboard_blocked">Blocked Funds</string>
<string name="dashboard_overdue">Overdue Financing</string>
<!-- Transfer -->
<string name="transfer_tab_quick">Quick Transfer</string>