forked from LibreMV/GridFlow
added band rates ui
This commit is contained in:
@@ -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)
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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> {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,19 @@
|
||||
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() {
|
||||
@@ -13,23 +21,225 @@ 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
|
||||
}
|
||||
|
||||
return root
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
||||
|
31
app/src/main/res/layout/item_band_rate.xml
Normal file
31
app/src/main/res/layout/item_band_rate.xml
Normal 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>
|
17
app/src/main/res/layout/item_selection_chip.xml
Normal file
17
app/src/main/res/layout/item_selection_chip.xml
Normal 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" />
|
@@ -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>
|
||||
|
@@ -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" }
|
||||
|
Reference in New Issue
Block a user