forked from LibreMV/GridFlow
draw usage graphs
This commit is contained in:
@@ -45,3 +45,76 @@ data class ValidationError(
|
||||
val param: String,
|
||||
val location: String
|
||||
)
|
||||
|
||||
// New models for customer subscriptions and usage
|
||||
data class CustomerSubscription(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val subscriptionId: Long,
|
||||
val createdAt: String,
|
||||
val deletedAt: String?,
|
||||
val customerId: Long,
|
||||
val subscription: SubscriptionDetails
|
||||
)
|
||||
|
||||
data class SubscriptionDetails(
|
||||
val id: Long,
|
||||
val subscriptionNumber: String,
|
||||
val status: String,
|
||||
val serviceId: Int, // 1 = Electrical, 2 = Water
|
||||
val hasSmartMeter: Boolean,
|
||||
val category: ServiceCategory,
|
||||
val customer: Customer,
|
||||
val lastBill: LastBill?
|
||||
)
|
||||
|
||||
data class ServiceCategory(
|
||||
val id: Int,
|
||||
val name: String, // "Domestic", "Business", etc.
|
||||
val code: String,
|
||||
val mainCategory: MainCategory
|
||||
)
|
||||
|
||||
data class MainCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val code: String
|
||||
)
|
||||
|
||||
data class Customer(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val accountNumber: String,
|
||||
val phone: String
|
||||
)
|
||||
|
||||
data class LastBill(
|
||||
val id: Long,
|
||||
val billNumber: String,
|
||||
val billAmount: String,
|
||||
val billedUnits: String,
|
||||
val dueDate: String,
|
||||
val billDate: String,
|
||||
val status: String,
|
||||
val type: String
|
||||
)
|
||||
|
||||
data class UsageReading(
|
||||
val meter: MeterReading,
|
||||
val currentReadingDate: String
|
||||
)
|
||||
|
||||
data class MeterReading(
|
||||
val currentUnits: String,
|
||||
val currentReadingDate: String
|
||||
)
|
||||
|
||||
// Outstanding bills data model
|
||||
data class OutstandingBill(
|
||||
val id: Long,
|
||||
val billNumber: String,
|
||||
val billAmount: String,
|
||||
val dueDate: String,
|
||||
val subscriptionId: Long,
|
||||
val serviceId: Int
|
||||
)
|
||||
|
@@ -8,13 +8,16 @@ import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import sh.sar.gridflow.data.CustomerSubscription
|
||||
import sh.sar.gridflow.data.ErrorResponse
|
||||
import sh.sar.gridflow.data.ForgotPasswordRequest
|
||||
import sh.sar.gridflow.data.ForgotPasswordResponse
|
||||
import sh.sar.gridflow.data.LoginRequest
|
||||
import sh.sar.gridflow.data.LoginResponse
|
||||
import sh.sar.gridflow.data.OutstandingBill
|
||||
import sh.sar.gridflow.data.SignupRequest
|
||||
import sh.sar.gridflow.data.SignupResponse
|
||||
import sh.sar.gridflow.data.UsageReading
|
||||
import java.io.IOException
|
||||
|
||||
class FenakaApiService {
|
||||
@@ -221,9 +224,9 @@ class FenakaApiService {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOutstandingBills(cookie: String): ApiResult<List<Any>> = withContext(Dispatchers.IO) {
|
||||
suspend fun getCustomerSubscriptions(cookie: String): ApiResult<List<CustomerSubscription>> = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/saiph/subscriptions/summaries/outstanding")
|
||||
.url("$BASE_URL/api/customer-subscriptions")
|
||||
.get()
|
||||
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -237,12 +240,11 @@ class FenakaApiService {
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val responseBody = response.body?.string() ?: "[]"
|
||||
// For now, we just need to know if it's empty or not
|
||||
val bills = gson.fromJson(responseBody, Array<Any>::class.java).toList()
|
||||
ApiResult.Success(bills, null)
|
||||
val subscriptions = gson.fromJson(responseBody, Array<CustomerSubscription>::class.java).toList()
|
||||
ApiResult.Success(subscriptions, null)
|
||||
}
|
||||
else -> {
|
||||
ApiResult.Error("Failed to fetch outstanding bills", response.code)
|
||||
ApiResult.Error("Failed to fetch customer subscriptions", response.code)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
@@ -251,6 +253,84 @@ class FenakaApiService {
|
||||
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSubscriptionUsage(subscriptionId: Long, cookie: String): ApiResult<List<UsageReading>> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Fetching usage for subscription: $subscriptionId")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/api/customer-subscriptions/$subscriptionId/billed-readings")
|
||||
.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, "Usage response code: ${response.code}")
|
||||
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val responseBody = response.body?.string() ?: "[]"
|
||||
Log.d(TAG, "Usage response body: $responseBody")
|
||||
val usageReadings = gson.fromJson(responseBody, Array<UsageReading>::class.java).toList()
|
||||
Log.d(TAG, "Parsed ${usageReadings.size} usage readings")
|
||||
ApiResult.Success(usageReadings, null)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Failed to fetch usage: ${response.code}")
|
||||
ApiResult.Error("Failed to fetch subscription usage", response.code)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Network error during usage fetch", e)
|
||||
ApiResult.Error("Network error: ${e.message}", -1)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unexpected error during usage fetch", e)
|
||||
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOutstandingBills(cookie: String): ApiResult<List<OutstandingBill>> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Fetching outstanding bills")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$BASE_URL/saiph/subscriptions/summaries/outstanding")
|
||||
.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, "Outstanding bills response code: ${response.code}")
|
||||
|
||||
when (response.code) {
|
||||
200 -> {
|
||||
val responseBody = response.body?.string() ?: "[]"
|
||||
Log.d(TAG, "Outstanding bills response: $responseBody")
|
||||
val bills = gson.fromJson(responseBody, Array<OutstandingBill>::class.java).toList()
|
||||
Log.d(TAG, "Found ${bills.size} outstanding bills")
|
||||
ApiResult.Success(bills, null)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Failed to fetch outstanding bills: ${response.code}")
|
||||
ApiResult.Error("Failed to fetch outstanding bills", response.code)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Network error during bills fetch", e)
|
||||
ApiResult.Error("Network error: ${e.message}", -1)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unexpected error during bills fetch", e)
|
||||
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ApiResult<T> {
|
||||
|
@@ -4,57 +4,159 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import sh.sar.gridflow.data.CustomerSubscription
|
||||
import sh.sar.gridflow.databinding.FragmentHomeBinding
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
|
||||
// This property is only valid between onCreateView and
|
||||
// onDestroyView.
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var homeViewModel: HomeViewModel
|
||||
private lateinit var subscriptionsAdapter: SubscriptionsAdapter
|
||||
private var selectedSubscriptionId: Long? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val homeViewModel =
|
||||
ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application))
|
||||
.get(HomeViewModel::class.java)
|
||||
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
val root: View = binding.root
|
||||
|
||||
val welcomeTextView: TextView = binding.textHome
|
||||
val billsStatusTextView: TextView = binding.textBillsStatus
|
||||
val progressBar = binding.progressBills
|
||||
|
||||
homeViewModel.welcomeText.observe(viewLifecycleOwner) {
|
||||
welcomeTextView.text = it
|
||||
homeViewModel = ViewModelProvider(
|
||||
this,
|
||||
ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
|
||||
).get(HomeViewModel::class.java)
|
||||
|
||||
setupViews()
|
||||
setupObservers()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
// Setup subscriptions RecyclerView
|
||||
subscriptionsAdapter = SubscriptionsAdapter { subscription ->
|
||||
// When subscription is clicked, load its usage chart
|
||||
selectedSubscriptionId = subscription.id
|
||||
subscriptionsAdapter.setSelectedSubscription(subscription.id)
|
||||
homeViewModel.loadUsageChart(subscription.id)
|
||||
updateSelectedSubscriptionInfo(subscription)
|
||||
}
|
||||
|
||||
homeViewModel.billsStatus.observe(viewLifecycleOwner) {
|
||||
billsStatusTextView.text = it
|
||||
binding.recyclerSubscriptions.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = subscriptionsAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
// Welcome text
|
||||
homeViewModel.welcomeText.observe(viewLifecycleOwner) { text ->
|
||||
binding.textHome.text = text
|
||||
}
|
||||
|
||||
homeViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
// Bills status
|
||||
homeViewModel.billsStatus.observe(viewLifecycleOwner) { status ->
|
||||
binding.textBillsStatus.text = status
|
||||
}
|
||||
|
||||
homeViewModel.isLoadingBills.observe(viewLifecycleOwner) { isLoading ->
|
||||
binding.progressBills.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
if (isLoading) {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
billsStatusTextView.text = "Checking bills status..."
|
||||
} else {
|
||||
progressBar.visibility = View.GONE
|
||||
binding.textBillsStatus.text = "Checking bills status..."
|
||||
}
|
||||
}
|
||||
|
||||
return root
|
||||
// Subscriptions
|
||||
homeViewModel.subscriptions.observe(viewLifecycleOwner) { subscriptions ->
|
||||
subscriptionsAdapter.updateSubscriptions(subscriptions)
|
||||
}
|
||||
|
||||
homeViewModel.isLoadingSubscriptions.observe(viewLifecycleOwner) { isLoading ->
|
||||
binding.progressSubscriptions.visibility = if (isLoading) View.VISIBLE else View.GONE
|
||||
binding.recyclerSubscriptions.visibility = if (isLoading) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
homeViewModel.subscriptionsError.observe(viewLifecycleOwner) { error ->
|
||||
if (error != null) {
|
||||
binding.textSubscriptionsError.visibility = View.VISIBLE
|
||||
binding.textSubscriptionsError.text = error
|
||||
binding.recyclerSubscriptions.visibility = View.GONE
|
||||
} else {
|
||||
binding.textSubscriptionsError.visibility = View.GONE
|
||||
binding.recyclerSubscriptions.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
// Chart data
|
||||
homeViewModel.chartData.observe(viewLifecycleOwner) { chartData ->
|
||||
val unit = homeViewModel.chartUnit.value ?: "Units"
|
||||
val serviceType = homeViewModel.chartServiceType.value ?: "Unknown"
|
||||
|
||||
binding.chartUsage.setData(chartData, unit, serviceType)
|
||||
binding.chartUsage.visibility = View.VISIBLE
|
||||
|
||||
// Update chart title
|
||||
binding.textChartTitle.text = "$serviceType Usage"
|
||||
|
||||
// Show chart legend
|
||||
binding.layoutChartLegend.visibility = View.VISIBLE
|
||||
binding.textChartUnit.text = unit
|
||||
|
||||
// Hide other states
|
||||
binding.layoutChartEmpty.visibility = View.GONE
|
||||
binding.layoutChartError.visibility = View.GONE
|
||||
binding.layoutChartLoading.visibility = View.GONE
|
||||
}
|
||||
|
||||
homeViewModel.isLoadingChart.observe(viewLifecycleOwner) { isLoading ->
|
||||
if (isLoading) {
|
||||
binding.layoutChartLoading.visibility = View.VISIBLE
|
||||
binding.chartUsage.visibility = View.GONE
|
||||
binding.layoutChartEmpty.visibility = View.GONE
|
||||
binding.layoutChartError.visibility = View.GONE
|
||||
binding.layoutChartLegend.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
homeViewModel.chartError.observe(viewLifecycleOwner) { error ->
|
||||
if (error != null) {
|
||||
binding.layoutChartError.visibility = View.VISIBLE
|
||||
binding.textChartError.text = error
|
||||
binding.chartUsage.visibility = View.GONE
|
||||
binding.layoutChartEmpty.visibility = View.GONE
|
||||
binding.layoutChartLoading.visibility = View.GONE
|
||||
binding.layoutChartLegend.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
homeViewModel.chartUnit.observe(viewLifecycleOwner) { unit ->
|
||||
binding.textChartUnit.text = unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedSubscriptionInfo(subscription: CustomerSubscription) {
|
||||
// Show the selected subscription info
|
||||
binding.layoutSelectedSubscription.visibility = View.VISIBLE
|
||||
|
||||
// Set service icon
|
||||
val icon = when (subscription.subscription.serviceId) {
|
||||
1 -> "⚡" // Electrical
|
||||
2 -> "💧" // Water
|
||||
else -> "🔌" // Unknown
|
||||
}
|
||||
binding.textSelectedIcon.text = icon
|
||||
|
||||
// Set subscription name (shortened)
|
||||
binding.textSelectedName.text = subscription.name
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,9 +7,14 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import sh.sar.gridflow.data.CustomerSubscription
|
||||
import sh.sar.gridflow.data.UsageReading
|
||||
import sh.sar.gridflow.network.ApiResult
|
||||
import sh.sar.gridflow.network.FenakaApiService
|
||||
import sh.sar.gridflow.ui.widgets.ChartDataPoint
|
||||
import sh.sar.gridflow.utils.SecureStorage
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@@ -26,17 +31,47 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val _billsStatus = MutableLiveData<String>()
|
||||
val billsStatus: LiveData<String> = _billsStatus
|
||||
|
||||
private val _isLoading = MutableLiveData<Boolean>()
|
||||
val isLoading: LiveData<Boolean> = _isLoading
|
||||
private val _isLoadingBills = MutableLiveData<Boolean>()
|
||||
val isLoadingBills: LiveData<Boolean> = _isLoadingBills
|
||||
|
||||
// Subscriptions
|
||||
private val _subscriptions = MutableLiveData<List<CustomerSubscription>>()
|
||||
val subscriptions: LiveData<List<CustomerSubscription>> = _subscriptions
|
||||
|
||||
private val _isLoadingSubscriptions = MutableLiveData<Boolean>()
|
||||
val isLoadingSubscriptions: LiveData<Boolean> = _isLoadingSubscriptions
|
||||
|
||||
private val _subscriptionsError = MutableLiveData<String?>()
|
||||
val subscriptionsError: LiveData<String?> = _subscriptionsError
|
||||
|
||||
// Usage Chart
|
||||
private val _chartData = MutableLiveData<List<ChartDataPoint>>()
|
||||
val chartData: LiveData<List<ChartDataPoint>> = _chartData
|
||||
|
||||
private val _chartUnit = MutableLiveData<String>()
|
||||
val chartUnit: LiveData<String> = _chartUnit
|
||||
|
||||
private val _chartServiceType = MutableLiveData<String>()
|
||||
val chartServiceType: LiveData<String> = _chartServiceType
|
||||
|
||||
private val _isLoadingChart = MutableLiveData<Boolean>()
|
||||
val isLoadingChart: LiveData<Boolean> = _isLoadingChart
|
||||
|
||||
private val _chartError = MutableLiveData<String?>()
|
||||
val chartError: LiveData<String?> = _chartError
|
||||
|
||||
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
|
||||
init {
|
||||
Log.d(TAG, "HomeViewModel initialized")
|
||||
loadUserData()
|
||||
checkOutstandingBills()
|
||||
loadCustomerSubscriptions()
|
||||
}
|
||||
|
||||
private fun loadUserData() {
|
||||
val userName = secureStorage.getUserName() ?: "User"
|
||||
val activeAccount = secureStorage.getActiveAccount()
|
||||
val userName = activeAccount?.name ?: "User"
|
||||
Log.d(TAG, "Loading user data: $userName")
|
||||
_welcomeText.value = "Welcome\n$userName"
|
||||
}
|
||||
@@ -52,14 +87,14 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
return
|
||||
}
|
||||
|
||||
_isLoading.value = true
|
||||
_isLoadingBills.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Making API call to check outstanding bills")
|
||||
when (val result = apiService.getOutstandingBills("connect.sid=$cookie")) {
|
||||
is ApiResult.Success -> {
|
||||
Log.d(TAG, "Bills check successful: ${result.data.size} bills found")
|
||||
_isLoading.value = false
|
||||
_isLoadingBills.value = false
|
||||
if (result.data.isEmpty()) {
|
||||
_billsStatus.value = "You don't have any pending bills left"
|
||||
} else {
|
||||
@@ -68,10 +103,117 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.d(TAG, "Bills check failed: ${result.message}")
|
||||
_isLoading.value = false
|
||||
_isLoadingBills.value = false
|
||||
_billsStatus.value = "Unable to check bills status"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCustomerSubscriptions() {
|
||||
val cookie = secureStorage.getCookie()
|
||||
if (cookie == null) {
|
||||
Log.d(TAG, "No cookie found, cannot load subscriptions")
|
||||
_subscriptionsError.value = "Authentication required"
|
||||
return
|
||||
}
|
||||
|
||||
_isLoadingSubscriptions.value = true
|
||||
_subscriptionsError.value = null
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Loading customer subscriptions")
|
||||
when (val result = apiService.getCustomerSubscriptions("connect.sid=$cookie")) {
|
||||
is ApiResult.Success -> {
|
||||
Log.d(TAG, "Subscriptions loaded successfully: ${result.data.size} found")
|
||||
_isLoadingSubscriptions.value = false
|
||||
_subscriptions.value = result.data
|
||||
|
||||
} is ApiResult.Error -> {
|
||||
Log.d(TAG, "Failed to load subscriptions: ${result.message}")
|
||||
_isLoadingSubscriptions.value = false
|
||||
_subscriptionsError.value = result.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadUsageChart(subscriptionId: Long) {
|
||||
val cookie = secureStorage.getCookie()
|
||||
if (cookie == null) {
|
||||
Log.d(TAG, "No cookie found, cannot load usage data")
|
||||
_chartError.value = "Authentication required"
|
||||
return
|
||||
}
|
||||
|
||||
// Find the subscription to get service type
|
||||
val subscription = _subscriptions.value?.find { it.id == subscriptionId }
|
||||
if (subscription == null) {
|
||||
Log.d(TAG, "Subscription not found: $subscriptionId")
|
||||
_chartError.value = "Subscription not found"
|
||||
return
|
||||
}
|
||||
|
||||
val serviceType = when (subscription.subscription.serviceId) {
|
||||
1 -> "Electrical"
|
||||
2 -> "Water"
|
||||
else -> "Unknown"
|
||||
}
|
||||
|
||||
val unit = when (subscription.subscription.serviceId) {
|
||||
1 -> "kWh"
|
||||
2 -> "Ltr"
|
||||
else -> "Units"
|
||||
}
|
||||
|
||||
_isLoadingChart.value = true
|
||||
_chartError.value = null
|
||||
_chartServiceType.value = serviceType
|
||||
_chartUnit.value = unit
|
||||
|
||||
viewModelScope.launch {
|
||||
Log.d(TAG, "Loading usage data for subscription: $subscriptionId")
|
||||
when (val result = apiService.getSubscriptionUsage(subscriptionId, "connect.sid=$cookie")) {
|
||||
is ApiResult.Success -> {
|
||||
Log.d(TAG, "Usage data loaded successfully: ${result.data.size} readings")
|
||||
_isLoadingChart.value = false
|
||||
|
||||
// Convert usage readings to chart data points
|
||||
val chartPoints = result.data.mapNotNull { reading ->
|
||||
try {
|
||||
val date = dateFormatter.parse(reading.meter.currentReadingDate.substring(0, 10))
|
||||
val value = reading.meter.currentUnits.toFloat()
|
||||
val calendar = Calendar.getInstance().apply { time = date }
|
||||
val monthYear = "${calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())} ${calendar.get(Calendar.YEAR)}"
|
||||
|
||||
date?.let {
|
||||
ChartDataPoint(
|
||||
date = it,
|
||||
value = value,
|
||||
label = monthYear
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing usage reading", e)
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.date } // Sort by date ascending
|
||||
|
||||
_chartData.value = chartPoints
|
||||
|
||||
} is ApiResult.Error -> {
|
||||
Log.d(TAG, "Failed to load usage data: ${result.message}")
|
||||
_isLoadingChart.value = false
|
||||
_chartError.value = result.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshData() {
|
||||
Log.d(TAG, "Refreshing all data")
|
||||
loadUserData()
|
||||
checkOutstandingBills()
|
||||
loadCustomerSubscriptions()
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,10 @@
|
||||
package sh.sar.gridflow.ui.home
|
||||
|
||||
data class SubscriptionDropdownItem(
|
||||
val id: Long,
|
||||
val displayName: String,
|
||||
val serviceType: String,
|
||||
val serviceId: Int
|
||||
) {
|
||||
override fun toString(): String = displayName
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
package sh.sar.gridflow.ui.home
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sh.sar.gridflow.data.CustomerSubscription
|
||||
import sh.sar.gridflow.databinding.ItemSubscriptionBinding
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
class SubscriptionsAdapter(
|
||||
private var subscriptions: List<CustomerSubscription> = emptyList(),
|
||||
private var selectedSubscriptionId: Long? = null,
|
||||
private val onSubscriptionClick: (CustomerSubscription) -> Unit = {}
|
||||
) : RecyclerView.Adapter<SubscriptionsAdapter.SubscriptionViewHolder>() {
|
||||
|
||||
fun updateSubscriptions(newSubscriptions: List<CustomerSubscription>) {
|
||||
subscriptions = newSubscriptions
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setSelectedSubscription(subscriptionId: Long?) {
|
||||
val oldSelectedIndex = subscriptions.indexOfFirst { it.id == selectedSubscriptionId }
|
||||
val newSelectedIndex = subscriptions.indexOfFirst { it.id == subscriptionId }
|
||||
|
||||
selectedSubscriptionId = subscriptionId
|
||||
|
||||
// Update old selection
|
||||
if (oldSelectedIndex != -1) {
|
||||
notifyItemChanged(oldSelectedIndex)
|
||||
}
|
||||
|
||||
// Update new selection
|
||||
if (newSelectedIndex != -1) {
|
||||
notifyItemChanged(newSelectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val binding = ItemSubscriptionBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return SubscriptionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
|
||||
holder.bind(subscriptions[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = subscriptions.size
|
||||
|
||||
inner class SubscriptionViewHolder(
|
||||
private val binding: ItemSubscriptionBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(subscription: CustomerSubscription) {
|
||||
with(binding) {
|
||||
// Check if this item is selected
|
||||
val isSelected = subscription.id == selectedSubscriptionId
|
||||
|
||||
// Show/hide selection indicator
|
||||
viewSelectionIndicator.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
|
||||
// Set subscription name
|
||||
textSubscriptionName.text = subscription.name
|
||||
|
||||
// Set subscription number
|
||||
textSubscriptionNumber.text = "Sub: ${subscription.subscription.subscriptionNumber}"
|
||||
|
||||
// Set service type and icon
|
||||
val serviceType = when (subscription.subscription.serviceId) {
|
||||
1 -> "Electrical"
|
||||
2 -> "Water"
|
||||
else -> "Unknown"
|
||||
}
|
||||
textServiceType.text = serviceType
|
||||
|
||||
// Set service icon
|
||||
val (icon, iconColor) = when (subscription.subscription.serviceId) {
|
||||
1 -> "⚡" to Color.parseColor("#FF9800") // Orange for electrical
|
||||
2 -> "💧" to Color.parseColor("#2196F3") // Blue for water
|
||||
else -> "🔌" to Color.parseColor("#4CAF50") // Green for unknown
|
||||
}
|
||||
textServiceIcon.text = icon
|
||||
|
||||
// Set last bill info
|
||||
val lastBill = subscription.subscription.lastBill
|
||||
if (lastBill != null) {
|
||||
val amount = try {
|
||||
lastBill.billAmount.toDouble()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
|
||||
val formatter = NumberFormat.getCurrencyInstance(Locale("en", "MV"))
|
||||
formatter.currency = Currency.getInstance("MVR")
|
||||
textBillAmount.text = formatter.format(amount)
|
||||
|
||||
// Set bill status with appropriate color
|
||||
val (statusText, statusColor) = when (lastBill.status.lowercase()) {
|
||||
"paid" -> "Paid" to Color.parseColor("#4CAF50")
|
||||
"pending" -> "Pending" to Color.parseColor("#FF9800")
|
||||
"overdue" -> "Overdue" to Color.parseColor("#F44336")
|
||||
else -> lastBill.status to Color.parseColor("#666666")
|
||||
}
|
||||
textBillStatus.text = statusText
|
||||
textBillStatus.setTextColor(statusColor)
|
||||
} else {
|
||||
textBillAmount.text = "No bill"
|
||||
textBillStatus.text = ""
|
||||
}
|
||||
|
||||
// Set click listener
|
||||
root.setOnClickListener {
|
||||
onSubscriptionClick(subscription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
app/src/main/java/sh/sar/gridflow/ui/widgets/BarChartView.kt
Normal file
195
app/src/main/java/sh/sar/gridflow/ui/widgets/BarChartView.kt
Normal file
@@ -0,0 +1,195 @@
|
||||
package sh.sar.gridflow.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class ChartDataPoint(
|
||||
val date: Date,
|
||||
val value: Float,
|
||||
val label: String
|
||||
)
|
||||
|
||||
class BarChartView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#2196F3")
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#666666")
|
||||
textSize = 28f
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val axisTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#999999")
|
||||
textSize = 24f
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val axisPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#EEEEEE")
|
||||
strokeWidth = 2f
|
||||
}
|
||||
|
||||
private var dataPoints: List<ChartDataPoint> = emptyList()
|
||||
private var unit: String = ""
|
||||
private var serviceType: String = ""
|
||||
|
||||
private val padding = 60f // Increased left padding for y-axis labels
|
||||
private val bottomPadding = 120f
|
||||
private val topPadding = 80f
|
||||
|
||||
private val monthFormatter = SimpleDateFormat("MMM", Locale.getDefault())
|
||||
|
||||
fun setData(data: List<ChartDataPoint>, unit: String, serviceType: String) {
|
||||
this.dataPoints = data.takeLast(10).reversed() // Show last 10 months, most recent first
|
||||
this.unit = unit
|
||||
this.serviceType = serviceType
|
||||
|
||||
// Update colors based on service type
|
||||
when (serviceType.lowercase()) {
|
||||
"electrical" -> {
|
||||
barPaint.color = Color.parseColor("#FF9800") // Orange for electrical
|
||||
}
|
||||
"water" -> {
|
||||
barPaint.color = Color.parseColor("#2196F3") // Blue for water
|
||||
}
|
||||
else -> {
|
||||
barPaint.color = Color.parseColor("#4CAF50") // Green default
|
||||
}
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (dataPoints.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val chartWidth = width - (padding * 2)
|
||||
val chartHeight = height - bottomPadding - topPadding
|
||||
|
||||
// Find max value for scaling
|
||||
val maxValue = dataPoints.maxOfOrNull { it.value } ?: 1f
|
||||
val niceMaxValue = getNiceMaxValue(maxValue)
|
||||
|
||||
// Draw y-axis labels and grid lines
|
||||
drawYAxisAndGrid(canvas, niceMaxValue, chartHeight)
|
||||
|
||||
// Calculate bar dimensions
|
||||
val barWidth = chartWidth / dataPoints.size * 0.8f
|
||||
val barSpacing = chartWidth / dataPoints.size * 0.2f
|
||||
|
||||
// Draw bars and x-axis labels
|
||||
dataPoints.forEachIndexed { index, dataPoint ->
|
||||
val barHeight = (dataPoint.value / niceMaxValue) * chartHeight
|
||||
val x = padding + (index * (barWidth + barSpacing)) + barSpacing / 2
|
||||
val y = topPadding + chartHeight - barHeight
|
||||
|
||||
// Draw bar
|
||||
val rect = RectF(x, y, x + barWidth, topPadding + chartHeight)
|
||||
canvas.drawRoundRect(rect, 8f, 8f, barPaint)
|
||||
|
||||
// Draw value on top of bar
|
||||
if (barHeight > 40f) { // Only show if bar is tall enough
|
||||
val valueText = formatValue(dataPoint.value)
|
||||
canvas.drawText(
|
||||
valueText,
|
||||
x + barWidth / 2,
|
||||
y - 8f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
// Draw month label
|
||||
val monthText = monthFormatter.format(dataPoint.date)
|
||||
canvas.drawText(
|
||||
monthText,
|
||||
x + barWidth / 2,
|
||||
height - 60f,
|
||||
axisTextPaint
|
||||
)
|
||||
|
||||
// Draw year if it's January or first/last item
|
||||
val calendar = Calendar.getInstance().apply { time = dataPoint.date }
|
||||
if (calendar.get(Calendar.MONTH) == Calendar.JANUARY || index == 0 || index == dataPoints.size - 1) {
|
||||
val yearText = calendar.get(Calendar.YEAR).toString()
|
||||
canvas.drawText(
|
||||
yearText,
|
||||
x + barWidth / 2,
|
||||
height - 32f,
|
||||
axisTextPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val text = "No usage data available"
|
||||
val x = width / 2f
|
||||
val y = height / 2f
|
||||
|
||||
textPaint.textAlign = Paint.Align.CENTER
|
||||
textPaint.color = Color.parseColor("#CCCCCC")
|
||||
canvas.drawText(text, x, y, textPaint)
|
||||
}
|
||||
|
||||
private fun drawYAxisAndGrid(canvas: Canvas, maxValue: Float, chartHeight: Float) {
|
||||
val steps = 5
|
||||
val stepValue = maxValue / steps
|
||||
|
||||
for (i in 0..steps) {
|
||||
val value = i * stepValue
|
||||
val y = topPadding + chartHeight - (value / maxValue * chartHeight)
|
||||
|
||||
// Draw grid line
|
||||
canvas.drawLine(padding, y, width - padding, y, axisPaint)
|
||||
|
||||
// Draw y-axis label
|
||||
val labelText = formatValue(value)
|
||||
axisTextPaint.textAlign = Paint.Align.RIGHT
|
||||
canvas.drawText(labelText, padding - 8f, y + 8f, axisTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNiceMaxValue(maxValue: Float): Float {
|
||||
if (maxValue <= 0) return 100f
|
||||
|
||||
val magnitude = Math.pow(10.0, Math.floor(Math.log10(maxValue.toDouble()))).toFloat()
|
||||
val normalized = maxValue / magnitude
|
||||
|
||||
val niceNormalized = when {
|
||||
normalized <= 1f -> 1f
|
||||
normalized <= 2f -> 2f
|
||||
normalized <= 5f -> 5f
|
||||
else -> 10f
|
||||
}
|
||||
|
||||
return niceNormalized * magnitude
|
||||
}
|
||||
|
||||
private fun formatValue(value: Float): String {
|
||||
return when {
|
||||
value >= 1000f -> "${(value / 1000f).roundToInt()}k"
|
||||
value >= 100f -> value.roundToInt().toString()
|
||||
value >= 10f -> "%.1f".format(value)
|
||||
else -> "%.2f".format(value)
|
||||
}
|
||||
}
|
||||
}
|
6
app/src/main/res/drawable/circle_background.xml
Normal file
6
app/src/main/res/drawable/circle_background.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/icon_circle_background" />
|
||||
<stroke android:width="1dp" android:color="@color/icon_circle_border" />
|
||||
</shape>
|
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/selected_subscription_background" />
|
||||
<stroke android:width="1dp" android:color="@color/selected_subscription_border" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
@@ -12,7 +12,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Welcome Section -->
|
||||
<TextView
|
||||
@@ -64,16 +64,237 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Placeholder for future features -->
|
||||
<TextView
|
||||
<!-- Subscriptions Overview Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_subscriptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="More features coming soon..."
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="32dp" />
|
||||
android:layout_marginBottom="24dp"
|
||||
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="Your Subscriptions"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_subscriptions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
tools:listitem="@layout/item_subscription" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_subscriptions"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscriptions_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Unable to load subscriptions"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Usage Chart Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_usage_chart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Chart Header -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_chart_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Usage Chart"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<!-- Selected Subscription Info -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_selected_subscription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone"
|
||||
android:padding="8dp"
|
||||
android:background="@drawable/selected_subscription_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_selected_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp"
|
||||
android:text="⚡" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_selected_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:maxWidth="120dp"
|
||||
android:text="Home" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Chart Container -->
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp">
|
||||
|
||||
<!-- Chart View -->
|
||||
<sh.sar.gridflow.ui.widgets.BarChartView
|
||||
android:id="@+id/chart_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Loading State -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_chart_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Loading usage data..."
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Empty State -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_chart_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<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="Select a subscription to view usage chart"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Error State -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_chart_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
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:id="@+id/text_chart_error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Unable to load usage data"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textAlignment="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Chart Legend -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_chart_legend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_chart_unit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="kWh"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
110
app/src/main/res/layout/item_subscription.xml
Normal file
110
app/src/main/res/layout/item_subscription.xml
Normal file
@@ -0,0 +1,110 @@
|
||||
<?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:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:id="@+id/layout_subscription_item">
|
||||
|
||||
<!-- Service Icon -->
|
||||
<TextView
|
||||
android:id="@+id/text_service_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/circle_background"
|
||||
android:gravity="center"
|
||||
android:textSize="16sp"
|
||||
tools:text="⚡" />
|
||||
|
||||
<!-- Subscription Details -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscription_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
tools:text="Thithilee, Hirundhu Magu, Kulhudhuffushi Electricity" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="2dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscription_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Sub: 8584" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=" • "
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_service_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Electrical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Last Bill Amount -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_bill_amount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="MVR 320.83" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_bill_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="2dp"
|
||||
tools:text="Paid"
|
||||
tools:textColor="#4CAF50" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Selection Indicator -->
|
||||
<View
|
||||
android:id="@+id/view_selection_indicator"
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="#2196F3"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
17
app/src/main/res/values-night/colors.xml
Normal file
17
app/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Material Design default colors for dark theme -->
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- Dark theme colors -->
|
||||
<color name="icon_circle_background">#2C2C2C</color>
|
||||
<color name="icon_circle_border">#555555</color>
|
||||
<color name="selected_subscription_background">#1E3A8A</color>
|
||||
<color name="selected_subscription_border">#3B82F6</color>
|
||||
</resources>
|
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Material Design default colors -->
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
@@ -7,4 +8,10 @@
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
|
||||
<!-- Light theme colors -->
|
||||
<color name="icon_circle_background">#E3F2FD</color>
|
||||
<color name="icon_circle_border">#BBDEFB</color>
|
||||
<color name="selected_subscription_background">#E3F2FD</color>
|
||||
<color name="selected_subscription_border">#90CAF9</color>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user