add support for mib loans view

This commit is contained in:
2026-05-14 05:24:18 +05:00
parent d28687bc1c
commit 69b7862e65
15 changed files with 896 additions and 4 deletions
@@ -0,0 +1,111 @@
package sh.sar.basedbank.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.databinding.ItemFinanceDealBinding
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class FinancingAdapter(private var deals: List<MibFinanceDeal>) :
RecyclerView.Adapter<FinancingAdapter.ViewHolder>() {
private val expandedPositions = mutableSetOf<Int>()
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()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemFinanceDealBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(deals[position], position in expandedPositions)
holder.binding.root.setOnClickListener {
val pos = holder.bindingAdapterPosition
if (pos in expandedPositions) expandedPositions.remove(pos) else expandedPositions.add(pos)
notifyItemChanged(pos)
}
}
override fun getItemCount() = deals.size
inner class ViewHolder(val binding: ItemFinanceDealBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(deal: MibFinanceDeal, expanded: Boolean) {
val ctx = binding.root.context
val currency = deal.currency
binding.tvProductName.text = deal.productDesc
binding.tvDealNo.text = ctx.getString(R.string.financing_deal_no_fmt, deal.dealNo)
binding.tvStatus.text = deal.statusDesc
binding.tvTotal.text = "$currency ${amountFmt.format(deal.dealAmount)}"
binding.tvPaid.text = "$currency ${amountFmt.format(deal.paidAmount)}"
binding.tvUnpaid.text = "$currency ${amountFmt.format(deal.outstandingAmount)}"
// Progress bar
val progress = if (deal.dealAmount > 0)
((deal.paidAmount / deal.dealAmount) * 100).toInt().coerceIn(0, 100)
else 0
binding.progressBar.progress = progress
// Completion estimate
binding.tvCompletion.text = completionText(deal, ctx)
// Expanded details
val detailsVisible = if (expanded) View.VISIBLE else View.GONE
binding.dividerDetails.visibility = detailsVisible
binding.detailsGroup.visibility = detailsVisible
if (expanded) {
binding.tvDealDate.text = formatDate(deal.dealDate)
binding.tvInstallment.text = "$currency ${amountFmt.format(deal.installmentAmount)}"
binding.tvNumInstallments.text = deal.noOfInstallments.toString()
binding.tvLastPaidDate.text = formatDate(deal.lastPaidDate)
binding.tvLastPayAmount.text = "$currency ${amountFmt.format(deal.lastPayAmount)}"
if (deal.overdueAmount > 0) {
binding.rowOverdue.visibility = View.VISIBLE
binding.tvOverdue.text = "$currency ${amountFmt.format(deal.overdueAmount)}"
} else {
binding.rowOverdue.visibility = View.GONE
}
}
}
private fun completionText(deal: MibFinanceDeal, ctx: android.content.Context): String {
if (deal.outstandingAmount <= 0.0) return ctx.getString(R.string.financing_completion_done)
val remaining = MibFinancingClient.remainingMonths(deal)
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 formatDate(raw: String): String {
return try {
outputDateFmt.format(inputDateFmt.parse(raw)!!)
} catch (_: Exception) {
raw.take(10)
}
}
}
}
@@ -0,0 +1,47 @@
package sh.sar.basedbank.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.basedbank.R
import sh.sar.basedbank.databinding.FragmentFinancingBinding
class FinancingFragment : Fragment() {
private var _binding: FragmentFinancingBinding? = null
private val binding get() = _binding!!
private val viewModel: HomeViewModel by activityViewModels()
private lateinit var adapter: FinancingAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFinancingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = FinancingAdapter(emptyList())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
viewModel.financing.observe(viewLifecycleOwner) { deals ->
adapter.updateDeals(deals)
binding.recyclerView.visibility = if (deals.isEmpty()) View.GONE else View.VISIBLE
binding.emptyView.visibility = if (deals.isEmpty()) View.VISIBLE else View.GONE
binding.loadingView.visibility = View.GONE
}
}
override fun onResume() {
super.onResume()
requireActivity().title = getString(R.string.nav_finances)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -17,8 +17,13 @@ import sh.sar.basedbank.R
import sh.sar.basedbank.api.mib.MibLoginFlow
import sh.sar.basedbank.databinding.ActivityHomeBinding
import sh.sar.basedbank.ui.login.LoginActivity
import sh.sar.basedbank.api.mib.MibFinancingClient
import sh.sar.basedbank.api.mib.MibProfile
import sh.sar.basedbank.api.mib.MibSession
import sh.sar.basedbank.api.mib.MibFinanceDeal
import sh.sar.basedbank.util.AccountCache
import sh.sar.basedbank.util.CredentialStore
import sh.sar.basedbank.util.FinancingCache
class HomeActivity : AppCompatActivity() {
@@ -44,6 +49,7 @@ class HomeActivity : AppCompatActivity() {
R.id.nav_dashboard -> show(DashboardFragment())
R.id.nav_add_account -> startActivity(Intent(this, LoginActivity::class.java))
R.id.nav_accounts -> show(AccountsFragment())
R.id.nav_finances -> show(FinancingFragment())
R.id.nav_settings -> show(SettingsFragment())
else -> Toast.makeText(this, R.string.work_in_progress, Toast.LENGTH_SHORT).show()
}
@@ -53,13 +59,18 @@ class HomeActivity : AppCompatActivity() {
// Load data
val app = application as BasedBankApp
if (app.accounts.isNotEmpty()) {
// Came from fresh manual login
// Came from fresh manual login — accounts ready, financing fetched in background
viewModel.accounts.value = app.accounts
AccountCache.save(this, app.accounts)
val cached = FinancingCache.load(this)
if (cached.isNotEmpty()) viewModel.financing.value = cached
refreshFinancing(app.mibSession, app.mibProfiles)
} else {
// Came from lock screen — show cache immediately, refresh in background
// Came from lock screen — show caches immediately, refresh everything in background
val cached = AccountCache.load(this)
if (cached.isNotEmpty()) viewModel.accounts.value = cached
val cachedFinancing = FinancingCache.load(this)
if (cachedFinancing.isNotEmpty()) viewModel.financing.value = cachedFinancing
val creds = CredentialStore(this).loadMibCredentials()
if (creds != null) autoRefresh(creds)
}
@@ -86,7 +97,10 @@ class HomeActivity : AppCompatActivity() {
val accounts = withContext(Dispatchers.IO) {
flow.login(creds.username, creds.passwordHash, creds.otpSeed)
}
(application as BasedBankApp).accounts = accounts
val app = application as BasedBankApp
app.accounts = accounts
app.mibSession = flow.lastSession
app.mibProfiles = flow.lastProfiles
AccountCache.save(this@HomeActivity, accounts)
viewModel.accounts.postValue(accounts)
} catch (_: Exception) {
@@ -94,6 +108,36 @@ class HomeActivity : AppCompatActivity() {
} finally {
binding.refreshIndicator.visibility = View.GONE
}
val app = application as BasedBankApp
refreshFinancing(app.mibSession, app.mibProfiles)
}
}
private fun refreshFinancing(session: MibSession?, profiles: List<MibProfile>) {
if (session == null || profiles.isEmpty()) return
val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE)
val flow = MibLoginFlow(prefs)
val client = MibFinancingClient()
lifecycleScope.launch {
try {
val deals = withContext(Dispatchers.IO) {
val allDeals = mutableListOf<MibFinanceDeal>()
val seen = mutableSetOf<String>()
for (profile in profiles) {
try {
flow.switchProfile(session, profile)
for (deal in client.fetchFinancing(session)) {
if (seen.add(deal.dealNo)) allDeals.add(deal)
}
} catch (_: Exception) { /* profile has no financing access */ }
}
allDeals
}
if (deals.isNotEmpty()) {
FinancingCache.save(this@HomeActivity, deals)
viewModel.financing.postValue(deals)
}
} catch (_: Exception) { /* keep cached data */ }
}
}
}
@@ -3,7 +3,9 @@ package sh.sar.basedbank.ui.home
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import sh.sar.basedbank.api.mib.MibAccount
import sh.sar.basedbank.api.mib.MibFinanceDeal
class HomeViewModel : ViewModel() {
val accounts = MutableLiveData<List<MibAccount>>(emptyList())
val financing = MutableLiveData<List<MibFinanceDeal>>(emptyList())
}
@@ -113,7 +113,10 @@ class CredentialsFragment : Fragment() {
Log.d(TAG, "Login succeeded, got ${accounts.size} accounts")
CredentialStore(requireContext()).saveMibCredentials(username, passwordHash, otpSeed)
AccountCache.save(requireContext(), accounts)
(requireActivity().application as BasedBankApp).accounts = accounts
val app = requireActivity().application as BasedBankApp
app.accounts = accounts
app.mibSession = flow.lastSession
app.mibProfiles = flow.lastProfiles
val intent = Intent(requireContext(), HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)