diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt new file mode 100644 index 0000000..5cd8bae --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibContactsClient.kt @@ -0,0 +1,155 @@ +package sh.sar.basedbank.api.mib + +import android.util.Log +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +class MibContactsClient { + + private val TAG = "MibContactsClient" + private val BASE_WV_URL = "https://faisamobilex-wv.mib.com.mv" + + private val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + private fun cookieHeader(session: MibSession) = + "mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " + + "mbnonce=${session.nonceGenerator}; time-tracker=597" + + private fun Request.Builder.withSessionHeaders(session: MibSession): Request.Builder = this + .header("Cookie", cookieHeader(session)) + .header( + "User-Agent", + "Mozilla/5.0 (Linux; Android 14; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.70 Mobile Safari/537.36" + ) + .header("X-Requested-With", "XMLHttpRequest") + .header("Accept", "*/*") + .header("Origin", BASE_WV_URL) + .header("Referer", "$BASE_WV_URL/beneficiary?dashurl=1") + + fun fetchCategories(session: MibSession): List { + val request = Request.Builder() + .url("$BASE_WV_URL/ajaxBeneficiary/getCategories") + .post(FormBody.Builder().build()) + .withSessionHeaders(session) + .build() + + return client.newCall(request).execute().use { response -> + Log.d(TAG, "getCategories: HTTP ${response.code}") + if (!response.isSuccessful) return emptyList() + parseCategories(response.body?.string() ?: return emptyList()) + } + } + + private fun parseCategories(json: String): List { + return try { + val arr = JSONObject(json).getJSONArray("data") + (0 until arr.length()).map { i -> + val o = arr.getJSONObject(i) + MibBeneficiaryCategory( + id = o.getString("id"), + categoryName = o.getString("categoryName"), + numBenef = o.optString("numBenef", "0").toIntOrNull() ?: 0 + ) + } + } catch (e: Exception) { + Log.e(TAG, "parseCategories error: $e") + emptyList() + } + } + + fun fetchContacts(session: MibSession): List { + val all = mutableListOf() + var page = 1 + val pageSize = 100 + while (true) { + val start = (page - 1) * pageSize + 1 + val end = page * pageSize + val body = FormBody.Builder() + .add("page", page.toString()) + .add("search", "") + .add("searchCategoryId", "0") + .add("benefType", "A") + .add("sortBenef", "name") + .add("sortDir", "asc") + .add("start", start.toString()) + .add("end", end.toString()) + .add("includeCount", "1") + .build() + + val request = Request.Builder() + .url("$BASE_WV_URL/ajaxBeneficiary/main") + .post(body) + .withSessionHeaders(session) + .build() + + val (contacts, totalCount) = client.newCall(request).execute().use { response -> + Log.d(TAG, "fetchContacts page $page: HTTP ${response.code}") + if (!response.isSuccessful) return all + parseContacts(response.body?.string() ?: return all) + } + all.addAll(contacts) + if (all.size >= totalCount || contacts.isEmpty()) break + page++ + } + Log.d(TAG, "fetchContacts: loaded ${all.size} contacts") + return all + } + + private fun parseContacts(json: String): Pair, Int> { + return try { + val obj = JSONObject(json) + val totalCount = obj.optString("total_count", "0").toIntOrNull() ?: 0 + val arr = obj.getJSONArray("data") + val contacts = (0 until arr.length()).map { i -> + val o = arr.getJSONObject(i) + val hash = o.optString("customerImgHash") + MibBeneficiary( + benefNo = o.optString("benefNo"), + benefName = o.optString("benefName"), + benefNickName = o.optString("benefNickName") + .ifBlank { o.optString("benefName") }, + benefAccount = o.optString("benefAccount"), + benefType = o.optString("benefType"), + bankColor = o.optString("bankColor", "#888888"), + benefBankName = o.optString("benefBankName"), + bankCode = o.optString("bankCode"), + benefStatus = o.optString("benefStatus"), + transferCyDesc = o.optString("transferCyDesc", "MVR"), + customerImgHash = hash.takeIf { it.isNotBlank() && it != "null" }, + benefCategoryId = o.optString("benefCategoryID", "0") + ) + } + Pair(contacts, totalCount) + } catch (e: Exception) { + Log.e(TAG, "parseContacts error: $e") + Pair(emptyList(), 0) + } + } + + fun fetchProfileImageBase64(session: MibSession, imageHash: String): String? { + val body = FormBody.Builder() + .add("imageHash", imageHash) + .build() + val request = Request.Builder() + .url("$BASE_WV_URL/ajaxBeneficiary/getProfileImage") + .post(body) + .withSessionHeaders(session) + .build() + + return client.newCall(request).execute().use { response -> + if (!response.isSuccessful) return null + try { + val json = response.body?.string() ?: return null + JSONObject(json).optString("profileImage").takeIf { it.isNotBlank() } + } catch (e: Exception) { + null + } + } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt index 26f049e..ba08594 100644 --- a/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt +++ b/app/src/main/java/sh/sar/basedbank/api/mib/MibModels.kt @@ -32,6 +32,27 @@ data class MibAccount( val statusDesc: String ) +data class MibBeneficiaryCategory( + val id: String, + val categoryName: String, + val numBenef: Int +) + +data class MibBeneficiary( + val benefNo: String, + val benefName: String, + val benefNickName: String, + val benefAccount: String, + val benefType: String, // L=Local, I=Internal(MIB), S=Swift + val bankColor: String, + val benefBankName: String, + val bankCode: String, + val benefStatus: String, + val transferCyDesc: String, + val customerImgHash: String?, + val benefCategoryId: String // "0" = uncategorized +) + data class MibFinanceDeal( val dealNo: String, val productDesc: String, diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt new file mode 100644 index 0000000..b5a1278 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsAdapter.kt @@ -0,0 +1,110 @@ +package sh.sar.basedbank.ui.home + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.databinding.ItemContactBinding + +class ContactsAdapter( + private val onImageNeeded: (hash: String) -> Unit +) : RecyclerView.Adapter() { + + private var allContacts: List = emptyList() + private var displayed: List = emptyList() + private val imageCache = mutableMapOf() + + private var activeCategoryId: String? = null + private var searchQuery: String = "" + + fun updateContacts(contacts: List) { + allContacts = contacts + applyFilter() + } + + fun updateImage(hash: String, bitmap: Bitmap) { + imageCache[hash] = bitmap + displayed.forEachIndexed { index, contact -> + if (contact.customerImgHash == hash) notifyItemChanged(index) + } + } + + fun setFilter(categoryId: String?, query: String) { + activeCategoryId = categoryId + searchQuery = query + applyFilter() + } + + private fun applyFilter() { + displayed = allContacts.filter { contact -> + val matchesCategory = activeCategoryId == null || contact.benefCategoryId == activeCategoryId + val matchesSearch = searchQuery.isBlank() || + contact.benefNickName.contains(searchQuery, ignoreCase = true) || + contact.benefName.contains(searchQuery, ignoreCase = true) || + contact.benefAccount.contains(searchQuery) + matchesCategory && matchesSearch + } + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemContactBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val contact = displayed[position] + val cachedImage = contact.customerImgHash?.let { hash -> + imageCache[hash] ?: run { onImageNeeded(hash); null } + } + holder.bind(contact, cachedImage) + } + + override fun getItemCount() = displayed.size + + inner class ViewHolder(private val binding: ItemContactBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(contact: MibBeneficiary, photo: Bitmap?) { + binding.tvContactName.text = contact.benefNickName + binding.tvContactBank.text = contact.benefBankName + binding.tvContactAccount.text = "${contact.benefAccount} · ${contact.transferCyDesc}" + + if (photo != null) { + binding.ivContactPhoto.setImageBitmap(photo) + } else { + binding.ivContactPhoto.setImageBitmap( + makeInitialsBitmap(contact.benefNickName, contact.bankColor) + ) + } + } + + private fun makeInitialsBitmap(name: String, colorHex: String): Bitmap { + val sizePx = binding.ivContactPhoto.context.resources + .getDimensionPixelSize(android.R.dimen.app_icon_size) + .coerceAtLeast(96) + val bgColor = try { Color.parseColor(colorHex) } catch (e: Exception) { Color.GRAY } + + val bm = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bm) + val paint = Paint(Paint.ANTI_ALIAS_FLAG) + + paint.color = bgColor + canvas.drawCircle(sizePx / 2f, sizePx / 2f, sizePx / 2f, paint) + + paint.color = Color.WHITE + paint.textSize = sizePx * 0.42f + paint.textAlign = Paint.Align.CENTER + val letter = name.firstOrNull()?.uppercaseChar()?.toString() ?: "?" + val metrics = paint.fontMetrics + val textY = sizePx / 2f - (metrics.ascent + metrics.descent) / 2f + canvas.drawText(letter, sizePx / 2f, textY, paint) + + return bm + } + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt new file mode 100644 index 0000000..42c31dc --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/ui/home/ContactsFragment.kt @@ -0,0 +1,123 @@ +package sh.sar.basedbank.ui.home + +import android.graphics.BitmapFactory +import android.os.Bundle +import android.util.Base64 +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.tabs.TabLayout +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import sh.sar.basedbank.BasedBankApp +import sh.sar.basedbank.R +import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.mib.MibContactsClient +import sh.sar.basedbank.databinding.FragmentContactsBinding + +class ContactsFragment : Fragment() { + + private var _binding: FragmentContactsBinding? = null + private val binding get() = _binding!! + private val viewModel: HomeViewModel by activityViewModels() + private lateinit var adapter: ContactsAdapter + + private val pendingHashes = mutableSetOf() + private val session get() = (requireActivity().application as BasedBankApp).mibSession + + private var categories: List = emptyList() + private var activeCategoryId: String? = null // null = All + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentContactsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + adapter = ContactsAdapter { hash -> fetchImage(hash) } + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + + binding.etSearch.addTextChangedListener { text -> + adapter.setFilter(activeCategoryId, text?.toString() ?: "") + } + + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + activeCategoryId = tab.tag as? String + adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "") + } + override fun onTabUnselected(tab: TabLayout.Tab) {} + override fun onTabReselected(tab: TabLayout.Tab) {} + }) + + viewModel.contactCategories.observe(viewLifecycleOwner) { cats -> + categories = cats + rebuildTabs(cats) + } + + viewModel.contacts.observe(viewLifecycleOwner) { contacts -> + adapter.updateContacts(contacts) + binding.recyclerView.visibility = if (contacts.isEmpty()) View.GONE else View.VISIBLE + binding.emptyView.visibility = if (contacts.isEmpty()) View.VISIBLE else View.GONE + binding.loadingView.visibility = View.GONE + } + } + + private fun rebuildTabs(cats: List) { + binding.tabLayout.clearOnTabSelectedListeners() + binding.tabLayout.removeAllTabs() + + binding.tabLayout.addTab( + binding.tabLayout.newTab().setText(R.string.contacts_tab_all).apply { tag = null } + ) + for (cat in cats) { + binding.tabLayout.addTab( + binding.tabLayout.newTab().setText(cat.categoryName).apply { tag = cat.id } + ) + } + + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + activeCategoryId = tab.tag as? String + adapter.setFilter(activeCategoryId, binding.etSearch.text?.toString() ?: "") + } + override fun onTabUnselected(tab: TabLayout.Tab) {} + override fun onTabReselected(tab: TabLayout.Tab) {} + }) + } + + private fun fetchImage(hash: String) { + if (!pendingHashes.add(hash)) return + val sess = session ?: return + val client = MibContactsClient() + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + try { + val base64 = client.fetchProfileImageBase64(sess, hash) ?: return@launch + val bytes = Base64.decode(base64, Base64.DEFAULT) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return@launch + withContext(Dispatchers.Main) { + adapter.updateImage(hash, bitmap) + } + } catch (_: Exception) { + pendingHashes.remove(hash) // allow retry + } + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.nav_contacts) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index ea571b1..2a90fce 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -17,11 +17,15 @@ 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.MibBeneficiary +import sh.sar.basedbank.api.mib.MibBeneficiaryCategory +import sh.sar.basedbank.api.mib.MibContactsClient 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.ContactsCache import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.FinancingCache @@ -49,6 +53,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_contacts -> show(ContactsFragment()) 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() @@ -59,18 +64,27 @@ class HomeActivity : AppCompatActivity() { // Load data val app = application as BasedBankApp if (app.accounts.isNotEmpty()) { - // Came from fresh manual login — accounts ready, financing fetched in background + // Came from fresh manual login — accounts ready, rest 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 + val cachedContacts = ContactsCache.loadContacts(this) + if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts + val cachedCats = ContactsCache.loadCategories(this) + if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats refreshFinancing(app.mibSession, app.mibProfiles) + refreshContacts(app.mibSession, app.mibProfiles) } else { // 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 cachedContacts = ContactsCache.loadContacts(this) + if (cachedContacts.isNotEmpty()) viewModel.contacts.value = cachedContacts + val cachedCats = ContactsCache.loadCategories(this) + if (cachedCats.isNotEmpty()) viewModel.contactCategories.value = cachedCats val creds = CredentialStore(this).loadMibCredentials() if (creds != null) autoRefresh(creds) } @@ -117,6 +131,41 @@ class HomeActivity : AppCompatActivity() { } val app = application as BasedBankApp refreshFinancing(app.mibSession, app.mibProfiles) + refreshContacts(app.mibSession, app.mibProfiles) + } + } + + private fun refreshContacts(session: MibSession?, profiles: List) { + if (session == null || profiles.isEmpty()) return + val prefs = getSharedPreferences("mib_prefs", MODE_PRIVATE) + val flow = MibLoginFlow(prefs) + val contactsClient = MibContactsClient() + lifecycleScope.launch { + try { + val (allContacts, allCategories) = withContext(Dispatchers.IO) { + val seenContacts = mutableSetOf() + val seenCategories = mutableSetOf() + val contacts = mutableListOf() + val categories = mutableListOf() + for (profile in profiles) { + try { + flow.switchProfile(session, profile) + for (cat in contactsClient.fetchCategories(session)) { + if (seenCategories.add(cat.id)) categories.add(cat) + } + for (contact in contactsClient.fetchContacts(session)) { + if (seenContacts.add(contact.benefNo)) contacts.add(contact) + } + } catch (_: Exception) { /* profile has no contacts access */ } + } + Pair(contacts, categories) + } + if (allContacts.isNotEmpty()) { + ContactsCache.save(this@HomeActivity, allContacts, allCategories) + viewModel.contacts.postValue(allContacts) + viewModel.contactCategories.postValue(allCategories) + } + } catch (_: Exception) { /* keep cached data */ } } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt index 80b3cb8..81bd524 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeViewModel.kt @@ -3,9 +3,13 @@ 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.MibBeneficiary +import sh.sar.basedbank.api.mib.MibBeneficiaryCategory import sh.sar.basedbank.api.mib.MibFinanceDeal class HomeViewModel : ViewModel() { val accounts = MutableLiveData>(emptyList()) val financing = MutableLiveData>(emptyList()) + val contacts = MutableLiveData>(emptyList()) + val contactCategories = MutableLiveData>(emptyList()) } diff --git a/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt new file mode 100644 index 0000000..3437842 --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/util/ContactsCache.kt @@ -0,0 +1,97 @@ +package sh.sar.basedbank.util + +import android.content.Context +import org.json.JSONArray +import org.json.JSONObject +import sh.sar.basedbank.api.mib.MibBeneficiary +import sh.sar.basedbank.api.mib.MibBeneficiaryCategory + +object ContactsCache { + + private const val PREFS = "contacts_cache" + private const val KEY_CONTACTS = "mib_contacts" + private const val KEY_CATEGORIES = "mib_categories" + + fun save( + context: Context, + contacts: List, + categories: List + ) { + val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + + val contactsArr = JSONArray() + for (c in contacts) { + contactsArr.put(JSONObject().apply { + put("benefNo", c.benefNo) + put("benefName", c.benefName) + put("benefNickName", c.benefNickName) + put("benefAccount", c.benefAccount) + put("benefType", c.benefType) + put("bankColor", c.bankColor) + put("benefBankName", c.benefBankName) + put("bankCode", c.bankCode) + put("benefStatus", c.benefStatus) + put("transferCyDesc", c.transferCyDesc) + put("customerImgHash", c.customerImgHash ?: "") + put("benefCategoryId", c.benefCategoryId) + }) + } + prefs.putString(KEY_CONTACTS, contactsArr.toString()) + + val catArr = JSONArray() + for (cat in categories) { + catArr.put(JSONObject().apply { + put("id", cat.id) + put("categoryName", cat.categoryName) + put("numBenef", cat.numBenef) + }) + } + prefs.putString(KEY_CATEGORIES, catArr.toString()) + prefs.apply() + } + + fun loadContacts(context: Context): List { + val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_CONTACTS, null) ?: return emptyList() + return try { + val arr = JSONArray(json) + (0 until arr.length()).map { i -> + val o = arr.getJSONObject(i) + MibBeneficiary( + benefNo = o.optString("benefNo"), + benefName = o.optString("benefName"), + benefNickName = o.optString("benefNickName"), + benefAccount = o.optString("benefAccount"), + benefType = o.optString("benefType"), + bankColor = o.optString("bankColor", "#888888"), + benefBankName = o.optString("benefBankName"), + bankCode = o.optString("bankCode"), + benefStatus = o.optString("benefStatus"), + transferCyDesc = o.optString("transferCyDesc", "MVR"), + customerImgHash = o.optString("customerImgHash").takeIf { it.isNotBlank() }, + benefCategoryId = o.optString("benefCategoryId", "0") + ) + } + } catch (e: Exception) { + emptyList() + } + } + + fun loadCategories(context: Context): List { + val json = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getString(KEY_CATEGORIES, null) ?: return emptyList() + return try { + val arr = JSONArray(json) + (0 until arr.length()).map { i -> + val o = arr.getJSONObject(i) + MibBeneficiaryCategory( + id = o.optString("id"), + categoryName = o.optString("categoryName"), + numBenef = o.optInt("numBenef", 0) + ) + } + } catch (e: Exception) { + emptyList() + } + } +} diff --git a/app/src/main/res/layout/fragment_contacts.xml b/app/src/main/res/layout/fragment_contacts.xml new file mode 100644 index 0000000..60c020f --- /dev/null +++ b/app/src/main/res/layout/fragment_contacts.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_contact.xml b/app/src/main/res/layout/item_contact.xml new file mode 100644 index 0000000..a3b2cbd --- /dev/null +++ b/app/src/main/res/layout/item_contact.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82bea67..702fb28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,11 @@ Amount Remarks + + No contacts found + Search contacts + All + No financing deals found Total diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index e43902e..c47bc14 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,4 +1,6 @@ +