forked from shihaam/thijooree
added support for BML loans
This commit is contained in:
@@ -49,6 +49,30 @@ class BmlAccountClient {
|
|||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchLoanDetail(session: BmlSession, internalId: String): BmlLoanDetail? {
|
||||||
|
val resp = client.newCall(bmlApiRequest(session, "$BML_BASE_URL/api/mobile/account/$internalId")).execute()
|
||||||
|
val code = resp.code
|
||||||
|
val json = resp.body?.string() ?: return null
|
||||||
|
resp.close()
|
||||||
|
if (code == 401 || code == 419) throw AuthExpiredException()
|
||||||
|
return try {
|
||||||
|
val root = JSONObject(json)
|
||||||
|
if (!root.optBoolean("success")) return null
|
||||||
|
val p = root.optJSONObject("payload") ?: return null
|
||||||
|
BmlLoanDetail(
|
||||||
|
loanAmount = p.optDouble("loanAmount", 0.0),
|
||||||
|
outstandingAmt = p.optDouble("outstandingAmt", 0.0),
|
||||||
|
repayAmount = p.optDouble("repayAmount", 0.0),
|
||||||
|
intRate = p.optDouble("intRate", 0.0),
|
||||||
|
loanStatus = p.optString("loanStatus"),
|
||||||
|
startDate = p.optString("startDate"),
|
||||||
|
endDate = p.optString("endDate"),
|
||||||
|
noOfRepayOverdue = p.optInt("noOfRepayOverdue", 0),
|
||||||
|
overdueAmount = p.optDouble("overdueAmount", 0.0)
|
||||||
|
)
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseDashboard(
|
private fun parseDashboard(
|
||||||
json: String,
|
json: String,
|
||||||
loginTag: String,
|
loginTag: String,
|
||||||
@@ -61,6 +85,7 @@ class BmlAccountClient {
|
|||||||
|
|
||||||
val casaAccounts = mutableListOf<BankAccount>()
|
val casaAccounts = mutableListOf<BankAccount>()
|
||||||
val prepaidCards = mutableListOf<BankAccount>()
|
val prepaidCards = mutableListOf<BankAccount>()
|
||||||
|
val loanAccounts = mutableListOf<BankAccount>()
|
||||||
|
|
||||||
for (i in 0 until dashboard.length()) {
|
for (i in 0 until dashboard.length()) {
|
||||||
val item = dashboard.getJSONObject(i)
|
val item = dashboard.getJSONObject(i)
|
||||||
@@ -91,6 +116,26 @@ class BmlAccountClient {
|
|||||||
profileId = profileId,
|
profileId = profileId,
|
||||||
internalId = internalId
|
internalId = internalId
|
||||||
))
|
))
|
||||||
|
} else if (accountType == "Loan") {
|
||||||
|
val outstanding = Math.abs(item.optDouble("availableBalance", 0.0))
|
||||||
|
loanAccounts.add(BankAccount(
|
||||||
|
bank = "BML",
|
||||||
|
profileName = profileName,
|
||||||
|
profileType = "BML_LOAN",
|
||||||
|
accountNumber = accountNumber,
|
||||||
|
accountBriefName = item.optString("alias"),
|
||||||
|
currencyName = currency,
|
||||||
|
accountTypeName = product,
|
||||||
|
availableBalance = "%.2f".format(outstanding),
|
||||||
|
currentBalance = "%.2f".format(outstanding),
|
||||||
|
blockedAmount = "0.00",
|
||||||
|
mvrBalance = "0.00",
|
||||||
|
statusDesc = status,
|
||||||
|
profileImageHash = null,
|
||||||
|
loginTag = loginTag,
|
||||||
|
profileId = profileId,
|
||||||
|
internalId = internalId
|
||||||
|
))
|
||||||
} else if (accountType == "Card") {
|
} else if (accountType == "Card") {
|
||||||
val isVisible = item.optBoolean("account_visible", false)
|
val isVisible = item.optBoolean("account_visible", false)
|
||||||
if (!isVisible) continue
|
if (!isVisible) continue
|
||||||
@@ -119,6 +164,6 @@ class BmlAccountClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return casaAccounts + prepaidCards
|
return casaAccounts + prepaidCards + loanAccounts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ data class BmlTransferResult(
|
|||||||
val errorMessage: String = ""
|
val errorMessage: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BmlLoanDetail(
|
||||||
|
val loanAmount: Double,
|
||||||
|
val outstandingAmt: Double, // negative as returned by API
|
||||||
|
val repayAmount: Double,
|
||||||
|
val intRate: Double,
|
||||||
|
val loanStatus: String,
|
||||||
|
val startDate: String, // ISO8601 e.g. "2023-10-26T00:00:00+05:00"
|
||||||
|
val endDate: String,
|
||||||
|
val noOfRepayOverdue: Int,
|
||||||
|
val overdueAmount: Double
|
||||||
|
)
|
||||||
|
|
||||||
data class BmlForeignLimit(
|
data class BmlForeignLimit(
|
||||||
val type: String,
|
val type: String,
|
||||||
val used: Double,
|
val used: Double,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import sh.sar.basedbank.R
|
|||||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
import kotlin.math.abs
|
||||||
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
import sh.sar.basedbank.databinding.FragmentDashboardBinding
|
||||||
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
import sh.sar.basedbank.databinding.ItemForeignLimitBinding
|
||||||
|
|
||||||
@@ -30,11 +31,12 @@ class DashboardFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
viewModel.accounts.observe(viewLifecycleOwner) { updateBalances(it) }
|
||||||
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances(it) }
|
viewModel.financing.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||||
|
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { updatePendingFinances() }
|
||||||
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
viewModel.bmlLimits.observe(viewLifecycleOwner) { updateForeignLimits(it) }
|
||||||
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
viewModel.hideAmounts.observe(viewLifecycleOwner) {
|
||||||
updateBalances(viewModel.accounts.value ?: emptyList())
|
updateBalances(viewModel.accounts.value ?: emptyList())
|
||||||
updatePendingFinances(viewModel.financing.value ?: emptyList())
|
updatePendingFinances()
|
||||||
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
|
updateForeignLimits(viewModel.bmlLimits.value ?: emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +125,13 @@ class DashboardFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingFinances(deals: List<MibFinanceDeal>) {
|
private fun updatePendingFinances() {
|
||||||
val hide = viewModel.hideAmounts.value ?: false
|
val hide = viewModel.hideAmounts.value ?: false
|
||||||
binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(deals.sumOf { it.outstandingAmount })
|
val mibTotal = (viewModel.financing.value ?: emptyList()).sumOf { it.outstandingAmount }
|
||||||
|
val bmlLoanDetails = viewModel.bmlLoanDetails.value ?: emptyMap()
|
||||||
|
val bmlTotal = bmlLoanDetails.values.sumOf { abs(it.outstandingAmt) }
|
||||||
|
val total = mibTotal + bmlTotal
|
||||||
|
binding.tvPendingFinances.text = if (hide) "MVR ••••••" else "MVR %,.2f".format(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -5,56 +5,97 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
import sh.sar.basedbank.api.mib.MibFinancingClient
|
import sh.sar.basedbank.api.mib.MibFinancingClient
|
||||||
|
import sh.sar.basedbank.databinding.ItemBmlLoanBinding
|
||||||
import sh.sar.basedbank.databinding.ItemFinanceDealBinding
|
import sh.sar.basedbank.databinding.ItemFinanceDealBinding
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
|
class FinancingAdapter(mibDeals: List<MibFinanceDeal>) :
|
||||||
RecyclerView.Adapter<FinancingAdapter.ViewHolder>() {
|
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
|
private sealed class Item {
|
||||||
|
data class Mib(val deal: MibFinanceDeal) : Item()
|
||||||
|
data class Bml(val account: BankAccount, val detail: BmlLoanDetail?) : Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var items: List<Item> = mibDeals.map { Item.Mib(it) }
|
||||||
private var hideAmounts: Boolean = false
|
private var hideAmounts: Boolean = false
|
||||||
|
|
||||||
|
private val expandedPositions = mutableSetOf<Int>()
|
||||||
|
private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply {
|
||||||
|
minimumFractionDigits = 2
|
||||||
|
maximumFractionDigits = 2
|
||||||
|
}
|
||||||
|
private val mibDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||||
|
private val isoDateFmt = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US)
|
||||||
|
private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US)
|
||||||
|
|
||||||
fun setHideAmounts(hide: Boolean) {
|
fun setHideAmounts(hide: Boolean) {
|
||||||
if (hideAmounts == hide) return
|
if (hideAmounts == hide) return
|
||||||
hideAmounts = hide
|
hideAmounts = hide
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val expandedPositions = mutableSetOf<Int>()
|
fun update(mibDeals: List<MibFinanceDeal>, bmlLoans: List<Pair<BankAccount, BmlLoanDetail?>>) {
|
||||||
private val amountFmt = NumberFormat.getNumberInstance(Locale.US).apply {
|
|
||||||
minimumFractionDigits = 2
|
|
||||||
maximumFractionDigits = 2
|
|
||||||
}
|
|
||||||
private val inputDateFmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
|
||||||
private val outputDateFmt = SimpleDateFormat("d MMM yyyy", Locale.US)
|
|
||||||
|
|
||||||
fun updateDeals(newDeals: List<MibFinanceDeal>) {
|
|
||||||
deals = newDeals
|
|
||||||
expandedPositions.clear()
|
expandedPositions.clear()
|
||||||
|
items = mibDeals.map { Item.Mib(it) } + bmlLoans.map { (acc, detail) -> Item.Bml(acc, detail) }
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
// Legacy compatibility — used on initial empty construction
|
||||||
val binding = ItemFinanceDealBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
fun updateDeals(newDeals: List<MibFinanceDeal>) {
|
||||||
return ViewHolder(binding)
|
expandedPositions.clear()
|
||||||
|
val bmlItems = items.filterIsInstance<Item.Bml>()
|
||||||
|
items = newDeals.map { Item.Mib(it) } + bmlItems
|
||||||
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
fun updateBmlLoans(loans: List<Pair<BankAccount, BmlLoanDetail?>>) {
|
||||||
holder.bind(deals[position], position in expandedPositions)
|
expandedPositions.clear()
|
||||||
holder.binding.root.setOnClickListener {
|
val mibItems = items.filterIsInstance<Item.Mib>()
|
||||||
|
items = mibItems + loans.map { (acc, detail) -> Item.Bml(acc, detail) }
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int) = when (items[position]) {
|
||||||
|
is Item.Mib -> TYPE_MIB
|
||||||
|
is Item.Bml -> TYPE_BML
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
return when (viewType) {
|
||||||
|
TYPE_BML -> BmlViewHolder(ItemBmlLoanBinding.inflate(inflater, parent, false))
|
||||||
|
else -> MibViewHolder(ItemFinanceDealBinding.inflate(inflater, parent, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
val expanded = position in expandedPositions
|
||||||
|
when (val item = items[position]) {
|
||||||
|
is Item.Mib -> (holder as MibViewHolder).bind(item.deal, expanded)
|
||||||
|
is Item.Bml -> (holder as BmlViewHolder).bind(item.account, item.detail, expanded)
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
val pos = holder.bindingAdapterPosition
|
val pos = holder.bindingAdapterPosition
|
||||||
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
|
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
|
||||||
notifyItemChanged(pos)
|
notifyItemChanged(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = deals.size
|
override fun getItemCount() = items.size
|
||||||
|
|
||||||
inner class ViewHolder(val binding: ItemFinanceDealBinding) :
|
// ── MIB ViewHolder ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
inner class MibViewHolder(val binding: ItemFinanceDealBinding) :
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun bind(deal: MibFinanceDeal, expanded: Boolean) {
|
fun bind(deal: MibFinanceDeal, expanded: Boolean) {
|
||||||
@@ -69,25 +110,22 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
|
|||||||
binding.tvPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.paidAmount)}"
|
binding.tvPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.paidAmount)}"
|
||||||
binding.tvUnpaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.outstandingAmount)}"
|
binding.tvUnpaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.outstandingAmount)}"
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
val progress = if (deal.dealAmount > 0)
|
val progress = if (deal.dealAmount > 0)
|
||||||
((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100)
|
((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100)
|
||||||
else 0
|
else 0
|
||||||
binding.progressBar.progress = if (hide) 0 else progress
|
binding.progressBar.progress = if (hide) 0 else progress
|
||||||
|
|
||||||
// Completion estimate
|
binding.tvCompletion.text = mibCompletionText(deal, ctx)
|
||||||
binding.tvCompletion.text = completionText(deal, ctx)
|
|
||||||
|
|
||||||
// Expanded details
|
|
||||||
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
|
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
|
||||||
binding.dividerDetails.visibility = detailsVisible
|
binding.dividerDetails.visibility = detailsVisible
|
||||||
binding.detailsGroup.visibility = detailsVisible
|
binding.detailsGroup.visibility = detailsVisible
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
binding.tvDealDate.text = formatDate(deal.dealDate)
|
binding.tvDealDate.text = formatMibDate(deal.dealDate)
|
||||||
binding.tvInstallment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.installmentAmount)}"
|
binding.tvInstallment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.installmentAmount)}"
|
||||||
binding.tvNumInstallments.text = deal.noOfInstallments.toString()
|
binding.tvNumInstallments.text = deal.noOfInstallments.toString()
|
||||||
binding.tvLastPaidDate.text = formatDate(deal.lastPaidDate)
|
binding.tvLastPaidDate.text = formatMibDate(deal.lastPaidDate)
|
||||||
binding.tvLastPayAmount.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.lastPayAmount)}"
|
binding.tvLastPayAmount.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(deal.lastPayAmount)}"
|
||||||
|
|
||||||
if (deal.overdueAmount > 0) {
|
if (deal.overdueAmount > 0) {
|
||||||
@@ -99,7 +137,7 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun completionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
|
private fun mibCompletionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
|
||||||
if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done)
|
if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done)
|
||||||
val remaining = MibFinancingClient.remainingMonths(deal)
|
val remaining = MibFinancingClient.remainingMonths(deal)
|
||||||
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
|
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
|
||||||
@@ -109,12 +147,84 @@ class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
|
|||||||
return ctx.getString(R.string.financing_completion_fmt, month)
|
return ctx.getString(R.string.financing_completion_fmt, month)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatDate(raw: String): String {
|
private fun formatMibDate(raw: String): String {
|
||||||
return try {
|
return try {
|
||||||
outputDateFmt.format(inputDateFmt.parse(raw)!!)
|
outputDateFmt.format(mibDateFmt.parse(raw)!!)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) { raw.take(10) }
|
||||||
raw.take(10)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── BML ViewHolder ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
inner class BmlViewHolder(val binding: ItemBmlLoanBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(account: BankAccount, detail: BmlLoanDetail?, expanded: Boolean) {
|
||||||
|
val ctx = binding.root.context
|
||||||
|
val currency = account.currencyName
|
||||||
|
val hide = hideAmounts
|
||||||
|
|
||||||
|
binding.tvLoanProduct.text = account.accountTypeName
|
||||||
|
.trim().lowercase().split(" ")
|
||||||
|
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercaseChar() } }
|
||||||
|
binding.tvLoanAccount.text = account.accountNumber
|
||||||
|
binding.tvLoanStatus.text = detail?.loanStatus?.ifBlank { account.statusDesc } ?: account.statusDesc
|
||||||
|
|
||||||
|
val loanAmt = detail?.loanAmount ?: 0.0
|
||||||
|
val outstanding = if (detail != null) abs(detail.outstandingAmt) else account.availableBalance.toDoubleOrNull() ?: 0.0
|
||||||
|
val paid = (loanAmt - outstanding).coerceAtLeast(0.0)
|
||||||
|
|
||||||
|
binding.tvLoanTotal.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(loanAmt)}"
|
||||||
|
binding.tvLoanPaid.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(paid)}"
|
||||||
|
binding.tvLoanOutstanding.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(outstanding)}"
|
||||||
|
|
||||||
|
val progress = if (loanAmt > 0) ((paid / loanAmt) * 100).toInt().coerceIn(0, 100) else 0
|
||||||
|
binding.loanProgressBar.progress = if (hide) 0 else progress
|
||||||
|
|
||||||
|
binding.tvLoanCompletion.text = bmlCompletionText(detail, ctx)
|
||||||
|
|
||||||
|
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
|
||||||
|
binding.loanDividerDetails.visibility = detailsVisible
|
||||||
|
binding.loanDetailsGroup.visibility = detailsVisible
|
||||||
|
|
||||||
|
if (expanded && detail != null) {
|
||||||
|
binding.tvLoanRepayment.text = if (hide) "$currency ••••••" else "$currency ${amountFmt.format(detail.repayAmount)}"
|
||||||
|
binding.tvLoanIntRate.text = ctx.getString(R.string.loan_rate_fmt, detail.intRate)
|
||||||
|
binding.tvLoanStartDate.text = formatIsoDate(detail.startDate)
|
||||||
|
binding.tvLoanEndDate.text = formatIsoDate(detail.endDate)
|
||||||
|
|
||||||
|
if (detail.overdueAmount > 0) {
|
||||||
|
binding.loanRowOverdue.visibility = View.VISIBLE
|
||||||
|
binding.tvLoanOverdue.text = if (hide) "$currency ••••••"
|
||||||
|
else "$currency ${amountFmt.format(detail.overdueAmount)} (${detail.noOfRepayOverdue})"
|
||||||
|
} else {
|
||||||
|
binding.loanRowOverdue.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bmlCompletionText(detail: BmlLoanDetail?, ctx: android.content.Context): String {
|
||||||
|
if (detail == null) return ""
|
||||||
|
val outstanding = abs(detail.outstandingAmt)
|
||||||
|
if (outstanding <= 0.0 || detail.repayAmount <= 0.0)
|
||||||
|
return ctx.getString(R.string.financing_completion_done)
|
||||||
|
val remaining = ceil(outstanding / detail.repayAmount).toInt()
|
||||||
|
if (remaining <= 0) return ctx.getString(R.string.financing_completion_done)
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
cal.add(Calendar.MONTH, remaining)
|
||||||
|
val month = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(cal.time)
|
||||||
|
return ctx.getString(R.string.financing_completion_fmt, month)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatIsoDate(raw: String): String {
|
||||||
|
return try {
|
||||||
|
outputDateFmt.format(isoDateFmt.parse(raw)!!)
|
||||||
|
} catch (_: Exception) { raw.take(10) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_MIB = 0
|
||||||
|
private const val TYPE_BML = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import sh.sar.basedbank.R
|
import sh.sar.basedbank.R
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||||
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.databinding.FragmentFinancingBinding
|
import sh.sar.basedbank.databinding.FragmentFinancingBinding
|
||||||
|
|
||||||
class FinancingFragment : Fragment() {
|
class FinancingFragment : Fragment() {
|
||||||
@@ -20,6 +23,9 @@ class FinancingFragment : Fragment() {
|
|||||||
private lateinit var adapter: FinancingAdapter
|
private lateinit var adapter: FinancingAdapter
|
||||||
private var financingRefreshing = false
|
private var financingRefreshing = false
|
||||||
|
|
||||||
|
private var latestMibDeals: List<MibFinanceDeal> = emptyList()
|
||||||
|
private var latestBmlLoanDetails: Map<String, BmlLoanDetail> = emptyMap()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = FragmentFinancingBinding.inflate(inflater, container, false)
|
_binding = FragmentFinancingBinding.inflate(inflater, container, false)
|
||||||
return binding.root
|
return binding.root
|
||||||
@@ -44,19 +50,36 @@ class FinancingFragment : Fragment() {
|
|||||||
(activity as? HomeActivity)?.triggerRefreshFinancing()
|
(activity as? HomeActivity)?.triggerRefreshFinancing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.accounts.observe(viewLifecycleOwner) { rebuildAdapter() }
|
||||||
viewModel.financing.observe(viewLifecycleOwner) { deals ->
|
viewModel.financing.observe(viewLifecycleOwner) { deals ->
|
||||||
adapter.updateDeals(deals)
|
latestMibDeals = deals
|
||||||
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
|
rebuildAdapter()
|
||||||
binding.emptyView.visibility = if (deals.isEmpty()) View.VISIBLE else View.GONE
|
}
|
||||||
binding.loadingView.visibility = View.GONE
|
viewModel.bmlLoanDetails.observe(viewLifecycleOwner) { details ->
|
||||||
if (financingRefreshing) {
|
latestBmlLoanDetails = details
|
||||||
financingRefreshing = false
|
rebuildAdapter()
|
||||||
binding.swipeRefresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
viewModel.hideAmounts.observe(viewLifecycleOwner) { adapter.setHideAmounts(it) }
|
viewModel.hideAmounts.observe(viewLifecycleOwner) { adapter.setHideAmounts(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rebuildAdapter() {
|
||||||
|
val accounts = viewModel.accounts.value ?: emptyList()
|
||||||
|
val loanAccounts = accounts.filter { it.profileType == "BML_LOAN" }
|
||||||
|
val bmlLoans: List<Pair<BankAccount, BmlLoanDetail?>> =
|
||||||
|
loanAccounts.map { acc -> acc to latestBmlLoanDetails[acc.internalId] }
|
||||||
|
|
||||||
|
adapter.update(latestMibDeals, bmlLoans)
|
||||||
|
|
||||||
|
val isEmpty = latestMibDeals.isEmpty() && bmlLoans.isEmpty()
|
||||||
|
binding.recyclerView.visibility = if (isEmpty) View.GONE else View.VISIBLE
|
||||||
|
binding.emptyView.visibility = if (isEmpty) View.VISIBLE else View.GONE
|
||||||
|
binding.loadingView.visibility = View.GONE
|
||||||
|
if (financingRefreshing) {
|
||||||
|
financingRefreshing = false
|
||||||
|
binding.swipeRefresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
requireActivity().title = getString(R.string.nav_finances)
|
requireActivity().title = getString(R.string.nav_finances)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import sh.sar.basedbank.api.bml.BmlAccountClient
|
|||||||
import sh.sar.basedbank.api.bml.BmlActivationResult
|
import sh.sar.basedbank.api.bml.BmlActivationResult
|
||||||
import sh.sar.basedbank.api.bml.BmlContactsClient
|
import sh.sar.basedbank.api.bml.BmlContactsClient
|
||||||
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
|
import sh.sar.basedbank.api.bml.BmlForeignLimitsClient
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||||
import sh.sar.basedbank.api.bml.BmlProfile
|
import sh.sar.basedbank.api.bml.BmlProfile
|
||||||
import sh.sar.basedbank.api.bml.BmlSession
|
import sh.sar.basedbank.api.bml.BmlSession
|
||||||
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
|
import sh.sar.basedbank.api.fahipay.FahipayAccountClient
|
||||||
@@ -170,6 +171,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
|
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
|
||||||
|
if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans
|
||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
@@ -178,6 +181,7 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
}
|
}
|
||||||
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
for ((_, session) in app.bmlSessions) refreshBmlLimits(session)
|
||||||
|
refreshBmlLoanDetails()
|
||||||
} else {
|
} else {
|
||||||
// Came from lock screen — show caches immediately, refresh everything in background
|
// Came from lock screen — show caches immediately, refresh everything in background
|
||||||
val store = CredentialStore(this)
|
val store = CredentialStore(this)
|
||||||
@@ -188,6 +192,8 @@ class HomeActivity : AppCompatActivity() {
|
|||||||
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
if (merged.isNotEmpty()) viewModel.accounts.value = merged
|
||||||
val cachedFinancing = FinancingCache.load(this)
|
val cachedFinancing = FinancingCache.load(this)
|
||||||
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
|
||||||
|
val cachedBmlLoans = FinancingCache.loadBmlLoans(this)
|
||||||
|
if (cachedBmlLoans.isNotEmpty()) viewModel.bmlLoanDetails.value = cachedBmlLoans
|
||||||
val cachedLimits = ForeignLimitsCache.load(this)
|
val cachedLimits = ForeignLimitsCache.load(this)
|
||||||
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
if (cachedLimits.isNotEmpty()) viewModel.bmlLimits.value = cachedLimits
|
||||||
|
|
||||||
@@ -334,6 +340,7 @@ fun applyNavLabelVisibility() {
|
|||||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
}
|
}
|
||||||
|
refreshBmlLoanDetails()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRefreshing(visible: Boolean) {
|
fun setRefreshing(visible: Boolean) {
|
||||||
@@ -661,6 +668,7 @@ fun applyNavLabelVisibility() {
|
|||||||
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
val profiles = app.mibProfilesMap[loginId] ?: emptyList()
|
||||||
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
refreshFinancing(loginId, session, profiles.filterVisibleProfiles(loginId))
|
||||||
}
|
}
|
||||||
|
refreshBmlLoanDetails()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,6 +906,32 @@ fun applyNavLabelVisibility() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun refreshBmlLoanDetails() {
|
||||||
|
val app = application as BasedBankApp
|
||||||
|
val loanAccounts = app.bmlAccounts.filter { it.profileType == "BML_LOAN" }
|
||||||
|
if (loanAccounts.isEmpty()) return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val details = withContext(Dispatchers.IO) {
|
||||||
|
val map = mutableMapOf<String, BmlLoanDetail>()
|
||||||
|
for (acc in loanAccounts) {
|
||||||
|
val session = app.bmlSessionFor(acc) ?: continue
|
||||||
|
try {
|
||||||
|
val detail = BmlAccountClient().fetchLoanDetail(session, acc.internalId)
|
||||||
|
if (detail != null) map[acc.internalId] = detail
|
||||||
|
} catch (_: Exception) { /* keep existing */ }
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
if (details.isNotEmpty()) {
|
||||||
|
val merged = (viewModel.bmlLoanDetails.value ?: emptyMap()) + details
|
||||||
|
FinancingCache.saveBmlLoans(this@HomeActivity, merged)
|
||||||
|
viewModel.bmlLoanDetails.postValue(merged)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { /* keep cached data */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
private fun refreshFinancing(loginId: String, session: MibSession, profiles: List<MibProfile>) {
|
||||||
if (profiles.isEmpty()) return
|
if (profiles.isEmpty()) return
|
||||||
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
val flow = (application as BasedBankApp).mibFlowFor(loginId)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package sh.sar.basedbank.ui.home
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
import sh.sar.basedbank.api.bml.BmlForeignLimit
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||||
import sh.sar.basedbank.api.models.BankAccount
|
import sh.sar.basedbank.api.models.BankAccount
|
||||||
import sh.sar.basedbank.api.models.BankContact
|
import sh.sar.basedbank.api.models.BankContact
|
||||||
import sh.sar.basedbank.api.models.BankContactCategory
|
import sh.sar.basedbank.api.models.BankContactCategory
|
||||||
@@ -11,6 +12,8 @@ import sh.sar.basedbank.api.mib.MibFinanceDeal
|
|||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
val accounts = MutableLiveData<List<BankAccount>>(emptyList())
|
||||||
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
|
||||||
|
/** BML loan details keyed by account internalId. */
|
||||||
|
val bmlLoanDetails = MutableLiveData<Map<String, BmlLoanDetail>>(emptyMap())
|
||||||
val contacts = MutableLiveData<List<BankContact>>(emptyList())
|
val contacts = MutableLiveData<List<BankContact>>(emptyList())
|
||||||
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
|
val contactCategories = MutableLiveData<List<BankContactCategory>>(emptyList())
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package sh.sar.basedbank.util
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import sh.sar.basedbank.api.bml.BmlLoanDetail
|
||||||
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
import sh.sar.basedbank.api.mib.MibFinanceDeal
|
||||||
|
|
||||||
object FinancingCache {
|
object FinancingCache {
|
||||||
|
|
||||||
private const val PREFS = "financing_cache"
|
private const val PREFS = "financing_cache"
|
||||||
private const val KEY_MIB = "mib_financing"
|
private const val KEY_MIB = "mib_financing"
|
||||||
|
private const val KEY_BML_LOANS = "bml_loans"
|
||||||
|
|
||||||
fun save(context: Context, deals: List<MibFinanceDeal>) {
|
fun save(context: Context, deals: List<MibFinanceDeal>) {
|
||||||
val arr = JSONArray()
|
val arr = JSONArray()
|
||||||
@@ -34,6 +36,52 @@ object FinancingCache {
|
|||||||
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
.edit().putString(KEY_MIB, CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveBmlLoans(context: Context, loans: Map<String, BmlLoanDetail>) {
|
||||||
|
val arr = JSONArray()
|
||||||
|
for ((internalId, d) in loans) {
|
||||||
|
arr.put(JSONObject().apply {
|
||||||
|
put("internalId", internalId)
|
||||||
|
put("loanAmount", d.loanAmount)
|
||||||
|
put("outstandingAmt", d.outstandingAmt)
|
||||||
|
put("repayAmount", d.repayAmount)
|
||||||
|
put("intRate", d.intRate)
|
||||||
|
put("loanStatus", d.loanStatus)
|
||||||
|
put("startDate", d.startDate)
|
||||||
|
put("endDate", d.endDate)
|
||||||
|
put("noOfRepayOverdue", d.noOfRepayOverdue)
|
||||||
|
put("overdueAmount", d.overdueAmount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit().putString(KEY_BML_LOANS, CacheEncryption.encrypt(arr.toString())).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBmlLoans(context: Context): Map<String, BmlLoanDetail> {
|
||||||
|
val raw = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_BML_LOANS, null) ?: return emptyMap()
|
||||||
|
return try {
|
||||||
|
val json = CacheEncryption.decrypt(raw)
|
||||||
|
val arr = JSONArray(json)
|
||||||
|
buildMap {
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
val id = o.optString("internalId")
|
||||||
|
if (id.isNotBlank()) put(id, BmlLoanDetail(
|
||||||
|
loanAmount = o.optDouble("loanAmount", 0.0),
|
||||||
|
outstandingAmt = o.optDouble("outstandingAmt", 0.0),
|
||||||
|
repayAmount = o.optDouble("repayAmount", 0.0),
|
||||||
|
intRate = o.optDouble("intRate", 0.0),
|
||||||
|
loanStatus = o.optString("loanStatus"),
|
||||||
|
startDate = o.optString("startDate"),
|
||||||
|
endDate = o.optString("endDate"),
|
||||||
|
noOfRepayOverdue = o.optInt("noOfRepayOverdue", 0),
|
||||||
|
overdueAmount = o.optDouble("overdueAmount", 0.0)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { emptyMap() }
|
||||||
|
}
|
||||||
|
|
||||||
fun clear(context: Context) {
|
fun clear(context: Context) {
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class HistoryFetcher(private val account: BankAccount) {
|
|||||||
|
|
||||||
private val isMib get() = account.bank == "MIB"
|
private val isMib get() = account.bank == "MIB"
|
||||||
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
private val isBmlCard get() = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
|
private val isBmlLoan get() = account.profileType == "BML_LOAN"
|
||||||
private val isFahipay get() = account.bank == "FAHIPAY"
|
private val isFahipay get() = account.bank == "FAHIPAY"
|
||||||
|
|
||||||
// MIB pagination
|
// MIB pagination
|
||||||
@@ -40,6 +41,7 @@ class HistoryFetcher(private val account: BankAccount) {
|
|||||||
private var fahipayTotal = -1
|
private var fahipayTotal = -1
|
||||||
|
|
||||||
fun hasMore(): Boolean = when {
|
fun hasMore(): Boolean = when {
|
||||||
|
isBmlLoan -> false
|
||||||
isFahipay -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
isFahipay -> fahipayTotal < 0 || fahipayNextStart < fahipayTotal
|
||||||
isMib -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
isMib -> mibTotalCount < 0 || mibNextStart <= mibTotalCount
|
||||||
isBmlCard -> cardMonthOffset < 3
|
isBmlCard -> cardMonthOffset < 3
|
||||||
@@ -47,6 +49,7 @@ class HistoryFetcher(private val account: BankAccount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<BankTransaction> = when {
|
suspend fun fetchNextPage(app: BasedBankApp, pageSize: Int = 10): List<BankTransaction> = when {
|
||||||
|
isBmlLoan -> emptyList()
|
||||||
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
|
isFahipay -> withContext(Dispatchers.IO) { fetchFahipay(app) }
|
||||||
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
|
isMib -> app.mibMutex.withLock { withContext(Dispatchers.IO) { fetchMib(app, pageSize) } }
|
||||||
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }
|
isBmlCard -> withContext(Dispatchers.IO) { fetchBmlCard(app) }
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ object BmlDashboardParser {
|
|||||||
* Returns all display fields for an account/card row in the accounts list.
|
* Returns all display fields for an account/card row in the accounts list.
|
||||||
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
* Handles both BML CASA accounts and BML prepaid/credit cards.
|
||||||
*/
|
*/
|
||||||
fun displayData(account: BankAccount): AccountListDisplay {
|
fun displayData(account: BankAccount): AccountListDisplay? {
|
||||||
|
if (account.profileType == "BML_LOAN") return null // Loans shown on financing page only
|
||||||
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
val isCard = account.profileType == "BML_PREPAID" || account.profileType == "BML_CREDIT"
|
||||||
return if (isCard) {
|
return if (isCard) {
|
||||||
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
val isActive = account.statusDesc.equals("Active", ignoreCase = true)
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
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:layout_marginBottom="12dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:cardElevation="0dp"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:strokeColor="?attr/colorOutlineVariant">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<!-- Header row: product name + status chip -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanProduct"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanAccount"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:background="@drawable/chip_background"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSecondaryContainer" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Loan amount -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/financing_total"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanTotal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/loanProgressBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:trackCornerRadius="4dp"
|
||||||
|
app:trackThickness="8dp" />
|
||||||
|
|
||||||
|
<!-- Paid / Outstanding row -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/financing_paid"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanPaid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="?attr/colorOnSurface"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/loan_outstanding"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelSmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanOutstanding"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||||
|
android:textColor="@color/color_unpaid"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Completion estimate -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanCompletion"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/loanDividerDetails"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?attr/colorOutlineVariant"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- Expanded details -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/loanDetailsGroup"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<!-- Monthly repayment -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/loan_monthly_repayment"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanRepayment"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Interest rate -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/loan_interest_rate"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanIntRate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Start date -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/loan_start_date"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanStartDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- End date -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/loan_end_date"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanEndDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurface" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Overdue (only shown when > 0) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/loanRowOverdue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/loan_overdue_payments"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="@color/color_unpaid" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLoanOverdue"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="@color/color_unpaid" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -261,4 +261,13 @@
|
|||||||
<string name="financing_completion_done">Fully paid</string>
|
<string name="financing_completion_done">Fully paid</string>
|
||||||
<string name="financing_deal_no_fmt">Deal #%s</string>
|
<string name="financing_deal_no_fmt">Deal #%s</string>
|
||||||
<string name="financing_completion_fmt">Completes %s</string>
|
<string name="financing_completion_fmt">Completes %s</string>
|
||||||
|
|
||||||
|
<!-- BML Loans -->
|
||||||
|
<string name="loan_outstanding">Outstanding</string>
|
||||||
|
<string name="loan_monthly_repayment">Monthly Repayment</string>
|
||||||
|
<string name="loan_interest_rate">Interest Rate</string>
|
||||||
|
<string name="loan_start_date">Start Date</string>
|
||||||
|
<string name="loan_end_date">End Date</string>
|
||||||
|
<string name="loan_overdue_payments">Overdue Payments</string>
|
||||||
|
<string name="loan_rate_fmt">%.2f%%</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user