added band rates ui

This commit is contained in:
2025-07-25 20:38:07 +05:00
parent b69d351578
commit a35583b024
12 changed files with 945 additions and 23 deletions

View File

@@ -53,6 +53,7 @@ dependencies {
implementation(libs.okhttp.logging)
implementation(libs.gson)
implementation(libs.security.crypto)
implementation(libs.flexbox)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -137,3 +137,24 @@ data class OutstandingBill(
val subscriptionId: Long,
val serviceId: Int
)
// Band rates data models
data class BandRatesResponse(
val waterGroups: List<WaterGroup>,
val electricityRates: Map<String, List<ElectricityZoneCategory>>
)
data class WaterGroup(
val category: String,
val rates: List<BandRate>
)
data class ElectricityZoneCategory(
val category: String,
val rates: List<BandRate>
)
data class BandRate(
val band: String,
val rate: String
)

View File

@@ -8,6 +8,7 @@ import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
import sh.sar.gridflow.data.BandRatesResponse
import sh.sar.gridflow.data.CustomerSubscription
import sh.sar.gridflow.data.ErrorResponse
import sh.sar.gridflow.data.ForgotPasswordRequest
@@ -331,6 +332,45 @@ class FenakaApiService {
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
suspend fun getBandRates(cookie: String): ApiResult<BandRatesResponse> = withContext(Dispatchers.IO) {
Log.d(TAG, "Fetching band rates")
val request = Request.Builder()
.url("$BASE_URL/api/band-rates")
.get()
.header("Authorization", "Bearer $BEARER_TOKEN")
.header("Content-Type", "application/json")
.header("Cookie", cookie)
.header("Host", "api.fenaka.mv")
.header("User-Agent", "Dart/3.3 (dart:io)")
.build()
try {
val response = client.newCall(request).execute()
Log.d(TAG, "Band rates response code: ${response.code}")
when (response.code) {
200 -> {
val responseBody = response.body?.string() ?: "{}"
Log.d(TAG, "Band rates response: $responseBody")
val bandRates = gson.fromJson(responseBody, BandRatesResponse::class.java)
Log.d(TAG, "Parsed band rates successfully")
ApiResult.Success(bandRates, null)
}
else -> {
Log.d(TAG, "Failed to fetch band rates: ${response.code}")
ApiResult.Error("Failed to fetch band rates", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during band rates fetch", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during band rates fetch", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
}
sealed class ApiResult<T> {

View File

@@ -0,0 +1,41 @@
package sh.sar.gridflow.ui.bandrates
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import sh.sar.gridflow.data.BandRate
import sh.sar.gridflow.databinding.ItemBandRateBinding
class BandRateAdapter : RecyclerView.Adapter<BandRateAdapter.BandRateViewHolder>() {
private var rates: List<BandRate> = emptyList()
fun updateRates(newRates: List<BandRate>) {
rates = newRates
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BandRateViewHolder {
val binding = ItemBandRateBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BandRateViewHolder(binding)
}
override fun onBindViewHolder(holder: BandRateViewHolder, position: Int) {
holder.bind(rates[position])
}
override fun getItemCount(): Int = rates.size
class BandRateViewHolder(private val binding: ItemBandRateBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(rate: BandRate) {
binding.textBand.text = rate.band
binding.textRate.text = rate.rate
}
}
}

View File

@@ -1,35 +1,245 @@
package sh.sar.gridflow.ui.bandrates
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import sh.sar.gridflow.R
import sh.sar.gridflow.databinding.FragmentBandRatesBinding
class BandRatesFragment : Fragment() {
private var _binding: FragmentBandRatesBinding? = null
private val binding get() = _binding!!
private lateinit var bandRatesViewModel: BandRatesViewModel
private lateinit var categoriesAdapter: SelectionChipAdapter
private lateinit var zonesAdapter: SelectionChipAdapter
private lateinit var ratesAdapter: BandRateAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val bandRatesViewModel =
ViewModelProvider(this).get(BandRatesViewModel::class.java)
bandRatesViewModel = ViewModelProvider(
this,
ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
).get(BandRatesViewModel::class.java)
_binding = FragmentBandRatesBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView = binding.textBandRates
bandRatesViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
setupViews()
setupObservers()
return binding.root
}
private fun setupViews() {
// Setup adapters - will be updated when tab changes
categoriesAdapter = SelectionChipAdapter({ category ->
val currentTab = bandRatesViewModel.selectedTab.value ?: 0
if (currentTab == 0) {
bandRatesViewModel.selectElectricityCategory(category)
} else {
bandRatesViewModel.selectWaterCategory(category)
}
}, true) // Default to electricity tab
zonesAdapter = SelectionChipAdapter({ formattedZone ->
// Convert formatted zone name back to original API format
val originalZone = bandRatesViewModel.getAvailableZones().find {
bandRatesViewModel.formatZoneName(it) == formattedZone
}
if (originalZone != null) {
bandRatesViewModel.selectZone(originalZone)
}
}, true) // Always electricity tab for zones
ratesAdapter = BandRateAdapter()
// Setup RecyclerViews with FlexboxLayoutManager for wrapping
binding.recyclerCategories.apply {
layoutManager = FlexboxLayoutManager(requireContext()).apply {
flexDirection = FlexDirection.ROW
flexWrap = FlexWrap.WRAP
justifyContent = JustifyContent.FLEX_START
}
adapter = categoriesAdapter
}
binding.recyclerZones.apply {
layoutManager = FlexboxLayoutManager(requireContext()).apply {
flexDirection = FlexDirection.ROW
flexWrap = FlexWrap.WRAP
justifyContent = JustifyContent.FLEX_START
}
adapter = zonesAdapter
}
binding.recyclerRates.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = ratesAdapter
}
// Setup tab clicks
binding.tabElectricity.setOnClickListener {
bandRatesViewModel.selectTab(0)
}
binding.tabWater.setOnClickListener {
bandRatesViewModel.selectTab(1)
}
}
private fun setupObservers() {
// Loading state
bandRatesViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
binding.layoutLoading.visibility = if (isLoading) View.VISIBLE else View.GONE
binding.layoutContent.visibility = if (isLoading) View.GONE else View.VISIBLE
binding.layoutError.visibility = View.GONE
}
// Error state
bandRatesViewModel.error.observe(viewLifecycleOwner) { error ->
if (error != null) {
binding.layoutError.visibility = View.VISIBLE
binding.textError.text = error
binding.layoutContent.visibility = View.GONE
binding.layoutLoading.visibility = View.GONE
} else {
binding.layoutError.visibility = View.GONE
}
}
// Tab selection
bandRatesViewModel.selectedTab.observe(viewLifecycleOwner) { tabIndex ->
updateTabUI(tabIndex)
recreateAdaptersForTab(tabIndex)
updateCategoriesAndZones()
}
// Data loaded
bandRatesViewModel.bandRatesData.observe(viewLifecycleOwner) { data ->
if (data != null) {
binding.layoutContent.visibility = View.VISIBLE
updateCategoriesAndZones()
}
}
// Zone selection (electricity only)
bandRatesViewModel.selectedZone.observe(viewLifecycleOwner) { _ ->
updateCategoriesAndZones()
}
// Category selections
bandRatesViewModel.selectedElectricityCategory.observe(viewLifecycleOwner) { _ ->
updateCategoriesAndZones()
}
bandRatesViewModel.selectedWaterCategory.observe(viewLifecycleOwner) { _ ->
updateCategoriesAndZones()
}
// Current rates
bandRatesViewModel.currentRates.observe(viewLifecycleOwner) { rates ->
ratesAdapter.updateRates(rates)
binding.recyclerRates.visibility = if (rates.isNotEmpty()) View.VISIBLE else View.GONE
binding.layoutRatesEmpty.visibility = if (rates.isEmpty()) View.VISIBLE else View.GONE
}
}
private fun recreateAdaptersForTab(tabIndex: Int) {
val isElectricityTab = tabIndex == 0
// Recreate categories adapter with correct tab colors
categoriesAdapter = SelectionChipAdapter({ category ->
val currentTab = bandRatesViewModel.selectedTab.value ?: 0
if (currentTab == 0) {
bandRatesViewModel.selectElectricityCategory(category)
} else {
bandRatesViewModel.selectWaterCategory(category)
}
}, isElectricityTab)
// Update RecyclerView with new adapter
binding.recyclerCategories.apply {
layoutManager = FlexboxLayoutManager(requireContext()).apply {
flexDirection = FlexDirection.ROW
flexWrap = FlexWrap.WRAP
justifyContent = JustifyContent.FLEX_START
}
adapter = categoriesAdapter
}
}
private fun updateTabUI(selectedTab: Int) {
if (selectedTab == 0) {
// Electricity tab selected
binding.tabElectricity.backgroundTintList = ContextCompat.getColorStateList(
requireContext(), R.color.electricity_color
)
binding.tabElectricity.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
binding.tabWater.backgroundTintList = ContextCompat.getColorStateList(
requireContext(), R.color.tab_inactive_color
)
binding.tabWater.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_inactive_text_color))
// Show zones card for electricity
binding.cardZones.visibility = View.VISIBLE
} else {
// Water tab selected
binding.tabWater.backgroundTintList = ContextCompat.getColorStateList(
requireContext(), R.color.water_color
)
binding.tabWater.setTextColor(ContextCompat.getColor(requireContext(), R.color.white))
binding.tabElectricity.backgroundTintList = ContextCompat.getColorStateList(
requireContext(), R.color.tab_inactive_color
)
binding.tabElectricity.setTextColor(ContextCompat.getColor(requireContext(), R.color.tab_inactive_text_color))
// Hide zones card for water
binding.cardZones.visibility = View.GONE
}
}
private fun updateCategoriesAndZones() {
val currentTab = bandRatesViewModel.selectedTab.value ?: 0
if (currentTab == 0) {
// Electricity tab
val zones = bandRatesViewModel.getAvailableZones()
val selectedZone = bandRatesViewModel.selectedZone.value
zonesAdapter.updateItems(
zones.map { bandRatesViewModel.formatZoneName(it) },
selectedZone?.let { bandRatesViewModel.formatZoneName(it) }
)
val categories = bandRatesViewModel.getAvailableElectricityCategories()
val selectedCategory = bandRatesViewModel.selectedElectricityCategory.value
categoriesAdapter.updateItems(categories, selectedCategory)
binding.textRatesTitle.text = "Electricity Rate Structure"
} else {
// Water tab
val categories = bandRatesViewModel.getAvailableWaterCategories()
val selectedCategory = bandRatesViewModel.selectedWaterCategory.value
categoriesAdapter.updateItems(categories, selectedCategory)
binding.textRatesTitle.text = "Water Rate Structure"
}
return root
}
override fun onDestroyView() {

View File

@@ -1,13 +1,200 @@
package sh.sar.gridflow.ui.bandrates
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import sh.sar.gridflow.data.BandRate
import sh.sar.gridflow.data.BandRatesResponse
import sh.sar.gridflow.network.ApiResult
import sh.sar.gridflow.network.FenakaApiService
import sh.sar.gridflow.utils.SecureStorage
class BandRatesViewModel : ViewModel() {
class BandRatesViewModel(application: Application) : AndroidViewModel(application) {
private val _text = MutableLiveData<String>().apply {
value = "Band Rates\n\nComing soon..."
private val secureStorage = SecureStorage(application)
private val apiService = FenakaApiService()
companion object {
private const val TAG = "BandRatesViewModel"
}
private val _bandRatesData = MutableLiveData<BandRatesResponse?>()
val bandRatesData: LiveData<BandRatesResponse?> = _bandRatesData
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
// Current tab - 0 for electricity, 1 for water
private val _selectedTab = MutableLiveData<Int>()
val selectedTab: LiveData<Int> = _selectedTab
// Electricity specific - zone and category selection
private val _selectedZone = MutableLiveData<String?>()
val selectedZone: LiveData<String?> = _selectedZone
private val _selectedElectricityCategory = MutableLiveData<String?>()
val selectedElectricityCategory: LiveData<String?> = _selectedElectricityCategory
// Water specific - category selection
private val _selectedWaterCategory = MutableLiveData<String?>()
val selectedWaterCategory: LiveData<String?> = _selectedWaterCategory
// Current rates to display
private val _currentRates = MutableLiveData<List<BandRate>>()
val currentRates: LiveData<List<BandRate>> = _currentRates
init {
Log.d(TAG, "BandRatesViewModel initialized")
_selectedTab.value = 0 // Default to electricity tab
loadBandRates()
}
private fun loadBandRates() {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Log.d(TAG, "No cookie found, cannot load band rates")
_error.value = "Authentication required"
return
}
_isLoading.value = true
_error.value = null
viewModelScope.launch {
Log.d(TAG, "Loading band rates")
when (val result = apiService.getBandRates("connect.sid=$cookie")) {
is ApiResult.Success -> {
Log.d(TAG, "Band rates loaded successfully")
_isLoading.value = false
_bandRatesData.value = result.data
initializeDefaultSelections()
} is ApiResult.Error -> {
Log.d(TAG, "Failed to load band rates: ${result.message}")
_isLoading.value = false
_error.value = result.message
}
}
}
}
private fun initializeDefaultSelections() {
val data = _bandRatesData.value ?: return
// Initialize electricity selections
val firstZone = data.electricityRates.keys.firstOrNull()
_selectedZone.value = firstZone
if (firstZone != null) {
val firstCategory = data.electricityRates[firstZone]?.firstOrNull()?.category
_selectedElectricityCategory.value = firstCategory
}
// Initialize water selections
val firstWaterCategory = data.waterGroups.firstOrNull()?.category
_selectedWaterCategory.value = firstWaterCategory
// Update current rates based on default tab (electricity)
updateCurrentRates()
}
fun selectTab(tabIndex: Int) {
_selectedTab.value = tabIndex
updateCurrentRates()
}
fun selectZone(zone: String) {
_selectedZone.value = zone
// Keep the current category if it exists in the new zone, otherwise pick the first one
val data = _bandRatesData.value ?: return
val currentCategory = _selectedElectricityCategory.value
val availableCategories = data.electricityRates[zone]?.map { it.category } ?: emptyList()
if (currentCategory != null && availableCategories.contains(currentCategory)) {
// Keep the current category since it's available in the new zone
_selectedElectricityCategory.value = currentCategory
} else {
// Fall back to first category if current one is not available
val firstCategory = availableCategories.firstOrNull()
_selectedElectricityCategory.value = firstCategory
}
updateCurrentRates()
}
fun selectElectricityCategory(category: String) {
_selectedElectricityCategory.value = category
updateCurrentRates()
}
fun selectWaterCategory(category: String) {
_selectedWaterCategory.value = category
updateCurrentRates()
}
private fun updateCurrentRates() {
val data = _bandRatesData.value ?: return
val currentTab = _selectedTab.value ?: 0
val rates = if (currentTab == 0) {
// Electricity tab
val zone = _selectedZone.value
val category = _selectedElectricityCategory.value
if (zone != null && category != null) {
data.electricityRates[zone]?.find { it.category == category }?.rates ?: emptyList()
} else {
emptyList()
}
} else {
// Water tab
val category = _selectedWaterCategory.value
if (category != null) {
data.waterGroups.find { it.category == category }?.rates ?: emptyList()
} else {
emptyList()
}
}
_currentRates.value = rates
}
fun formatZoneName(zoneName: String): String {
return zoneName
.replace("-", " ")
.split(" ")
.joinToString(" ") { word ->
if (word.isNotEmpty()) {
word.first().uppercaseChar() + word.drop(1).lowercase()
} else {
word
}
}
}
fun getAvailableZones(): List<String> {
return _bandRatesData.value?.electricityRates?.keys?.toList() ?: emptyList()
}
fun getAvailableElectricityCategories(): List<String> {
val data = _bandRatesData.value ?: return emptyList()
val zone = _selectedZone.value ?: return emptyList()
return data.electricityRates[zone]?.map { it.category } ?: emptyList()
}
fun getAvailableWaterCategories(): List<String> {
return _bandRatesData.value?.waterGroups?.map { it.category } ?: emptyList()
}
fun refreshData() {
Log.d(TAG, "Refreshing band rates data")
loadBandRates()
}
val text: LiveData<String> = _text
}

View File

@@ -0,0 +1,93 @@
package sh.sar.gridflow.ui.bandrates
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import sh.sar.gridflow.R
import sh.sar.gridflow.databinding.ItemSelectionChipBinding
class SelectionChipAdapter(
private val onItemClick: (String) -> Unit,
private val isElectricityTab: Boolean = false
) : RecyclerView.Adapter<SelectionChipAdapter.ChipViewHolder>() {
private var items: List<String> = emptyList()
private var selectedItem: String? = null
fun updateItems(newItems: List<String>, selected: String? = null) {
items = newItems
selectedItem = selected
notifyDataSetChanged()
}
fun setSelectedItem(item: String?) {
val previousIndex = selectedItem?.let { items.indexOf(it) } ?: -1
val newIndex = item?.let { items.indexOf(it) } ?: -1
selectedItem = item
if (previousIndex >= 0) notifyItemChanged(previousIndex)
if (newIndex >= 0) notifyItemChanged(newIndex)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChipViewHolder {
val binding = ItemSelectionChipBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ChipViewHolder(binding, onItemClick, isElectricityTab)
}
override fun onBindViewHolder(holder: ChipViewHolder, position: Int) {
holder.bind(items[position], items[position] == selectedItem)
}
override fun getItemCount(): Int = items.size
class ChipViewHolder(
private val binding: ItemSelectionChipBinding,
private val onItemClick: (String) -> Unit,
private val isElectricityTab: Boolean
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: String, isSelected: Boolean) {
binding.textChip.text = item
// Set background and text color based on selection and tab
if (isSelected) {
if (isElectricityTab) {
binding.textChip.backgroundTintList = ContextCompat.getColorStateList(
binding.root.context,
R.color.electricity_selection_background
)
binding.textChip.setTextColor(
ContextCompat.getColor(binding.root.context, R.color.electricity_selection_border)
)
} else {
binding.textChip.backgroundTintList = ContextCompat.getColorStateList(
binding.root.context,
R.color.water_selection_background
)
binding.textChip.setTextColor(
ContextCompat.getColor(binding.root.context, R.color.water_selection_border)
)
}
} else {
binding.textChip.backgroundTintList = ContextCompat.getColorStateList(
binding.root.context,
R.color.category_tag_background
)
binding.textChip.setTextColor(
ContextCompat.getColor(binding.root.context, R.color.tab_inactive_text_color)
)
}
binding.textChip.setOnClickListener {
onItemClick(item)
}
}
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -11,18 +12,284 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:gravity="center">
android:padding="16dp">
<TextView
android:id="@+id/text_band_rates"
<!-- Tab Layout -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginTop="100dp"
tools:text="Band Rates\n\nComing soon..." />
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="4dp">
<TextView
android:id="@+id/tab_electricity"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="Electricity"
android:textSize="16sp"
android:textStyle="bold"
android:gravity="center"
android:layout_margin="4dp"
android:background="@drawable/card_background"
android:clickable="true"
android:focusable="true"
tools:textColor="@color/white"
tools:backgroundTint="@color/electricity_color" />
<TextView
android:id="@+id/tab_water"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:text="Water"
android:textSize="16sp"
android:textStyle="bold"
android:gravity="center"
android:layout_margin="4dp"
android:background="@drawable/card_background"
android:clickable="true"
android:focusable="true"
tools:textColor="?android:attr/textColorPrimary"
tools:backgroundTint="@color/tab_inactive_color" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Content Area -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Loading State -->
<LinearLayout
android:id="@+id/layout_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="48dp"
android:visibility="gone"
tools:visibility="visible">
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Loading band rates..."
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<!-- Error State -->
<LinearLayout
android:id="@+id/layout_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="48dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚠️"
android:textSize="48sp"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/text_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Unable to load band rates"
android:textColor="?android:attr/textColorSecondary"
android:textAlignment="center" />
</LinearLayout>
<!-- Main Content -->
<LinearLayout
android:id="@+id/layout_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<!-- Category Selection -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Categories"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="12dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_subscription" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Zone Selection (Electricity Only) -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_zones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Zones"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="12dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_zones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_subscription" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Rates Table -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/text_rates_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Rate Structure"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp" />
<!-- Table Header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/category_tag_background"
android:padding="12dp"
android:layout_marginBottom="1dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Allocated Units"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Rate"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="end" />
</LinearLayout>
<!-- Rates List -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_rates"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- Empty State -->
<LinearLayout
android:id="@+id/layout_rates_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📊"
android:textSize="48sp"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No rates available for selected criteria"
android:textColor="?android:attr/textColorSecondary"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:layout_marginBottom="1dp"
android:background="@color/card_background_color">
<TextView
android:id="@+id/text_band"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
tools:text="0-100" />
<TextView
android:id="@+id/text_rate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:gravity="end"
tools:text="MVR 22.00" />
</LinearLayout>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text_chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:padding="12dp"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:background="@drawable/category_tag_background"
android:clickable="true"
android:focusable="true"
android:minWidth="80dp"
android:gravity="center"
tools:text="Domestic" />

View File

@@ -17,4 +17,16 @@
<color name="category_tag_background">#E0E0E0</color>
<color name="card_stroke_color">#E0E0E0</color>
<color name="card_background_color">#FFFFFF</color>
<!-- Band rates colors (matching chart colors) -->
<color name="electricity_color">#FF9800</color>
<color name="water_color">#2196F3</color>
<color name="tab_inactive_color">#E0E0E0</color>
<color name="tab_inactive_text_color">#666666</color>
<!-- Selection chip colors -->
<color name="electricity_selection_background">#FFF3E0</color>
<color name="electricity_selection_border">#FF9800</color>
<color name="water_selection_background">#E3F2FD</color>
<color name="water_selection_border">#2196F3</color>
</resources>

View File

@@ -16,6 +16,7 @@ okhttp = "4.12.0"
gson = "2.10.1"
security = "1.1.0-alpha06"
coordinatorlayout = "1.2.0"
flexbox = "3.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -34,6 +35,7 @@ okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor",
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
androidx-coordinatorlayout = { group = "androidx.coordinatorlayout", name = "coordinatorlayout", version.ref = "coordinatorlayout" }
flexbox = { group = "com.google.android.flexbox", name = "flexbox", version.ref = "flexbox" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }