draw usage graphs
This commit is contained in:
@@ -45,3 +45,76 @@ data class ValidationError(
|
|||||||
val param: String,
|
val param: String,
|
||||||
val location: 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.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import sh.sar.gridflow.data.CustomerSubscription
|
||||||
import sh.sar.gridflow.data.ErrorResponse
|
import sh.sar.gridflow.data.ErrorResponse
|
||||||
import sh.sar.gridflow.data.ForgotPasswordRequest
|
import sh.sar.gridflow.data.ForgotPasswordRequest
|
||||||
import sh.sar.gridflow.data.ForgotPasswordResponse
|
import sh.sar.gridflow.data.ForgotPasswordResponse
|
||||||
import sh.sar.gridflow.data.LoginRequest
|
import sh.sar.gridflow.data.LoginRequest
|
||||||
import sh.sar.gridflow.data.LoginResponse
|
import sh.sar.gridflow.data.LoginResponse
|
||||||
|
import sh.sar.gridflow.data.OutstandingBill
|
||||||
import sh.sar.gridflow.data.SignupRequest
|
import sh.sar.gridflow.data.SignupRequest
|
||||||
import sh.sar.gridflow.data.SignupResponse
|
import sh.sar.gridflow.data.SignupResponse
|
||||||
|
import sh.sar.gridflow.data.UsageReading
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class FenakaApiService {
|
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()
|
val request = Request.Builder()
|
||||||
.url("$BASE_URL/saiph/subscriptions/summaries/outstanding")
|
.url("$BASE_URL/api/customer-subscriptions")
|
||||||
.get()
|
.get()
|
||||||
.header("Authorization", "Bearer $BEARER_TOKEN")
|
.header("Authorization", "Bearer $BEARER_TOKEN")
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
@@ -237,12 +240,11 @@ class FenakaApiService {
|
|||||||
when (response.code) {
|
when (response.code) {
|
||||||
200 -> {
|
200 -> {
|
||||||
val responseBody = response.body?.string() ?: "[]"
|
val responseBody = response.body?.string() ?: "[]"
|
||||||
// For now, we just need to know if it's empty or not
|
val subscriptions = gson.fromJson(responseBody, Array<CustomerSubscription>::class.java).toList()
|
||||||
val bills = gson.fromJson(responseBody, Array<Any>::class.java).toList()
|
ApiResult.Success(subscriptions, null)
|
||||||
ApiResult.Success(bills, null)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ApiResult.Error("Failed to fetch outstanding bills", response.code)
|
ApiResult.Error("Failed to fetch customer subscriptions", response.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@@ -251,6 +253,84 @@ class FenakaApiService {
|
|||||||
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
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> {
|
sealed class ApiResult<T> {
|
||||||
|
@@ -4,57 +4,159 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import sh.sar.gridflow.data.CustomerSubscription
|
||||||
import sh.sar.gridflow.databinding.FragmentHomeBinding
|
import sh.sar.gridflow.databinding.FragmentHomeBinding
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: FragmentHomeBinding? = null
|
private var _binding: FragmentHomeBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
|
||||||
// onDestroyView.
|
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var homeViewModel: HomeViewModel
|
||||||
|
private lateinit var subscriptionsAdapter: SubscriptionsAdapter
|
||||||
|
private var selectedSubscriptionId: Long? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val homeViewModel =
|
|
||||||
ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application))
|
|
||||||
.get(HomeViewModel::class.java)
|
|
||||||
|
|
||||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
_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) {
|
homeViewModel = ViewModelProvider(
|
||||||
welcomeTextView.text = it
|
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) {
|
binding.recyclerSubscriptions.apply {
|
||||||
billsStatusTextView.text = it
|
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) {
|
if (isLoading) {
|
||||||
progressBar.visibility = View.VISIBLE
|
binding.textBillsStatus.text = "Checking bills status..."
|
||||||
billsStatusTextView.text = "Checking bills status..."
|
|
||||||
} else {
|
|
||||||
progressBar.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,14 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
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.ApiResult
|
||||||
import sh.sar.gridflow.network.FenakaApiService
|
import sh.sar.gridflow.network.FenakaApiService
|
||||||
|
import sh.sar.gridflow.ui.widgets.ChartDataPoint
|
||||||
import sh.sar.gridflow.utils.SecureStorage
|
import sh.sar.gridflow.utils.SecureStorage
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
@@ -26,17 +31,47 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
private val _billsStatus = MutableLiveData<String>()
|
private val _billsStatus = MutableLiveData<String>()
|
||||||
val billsStatus: LiveData<String> = _billsStatus
|
val billsStatus: LiveData<String> = _billsStatus
|
||||||
|
|
||||||
private val _isLoading = MutableLiveData<Boolean>()
|
private val _isLoadingBills = MutableLiveData<Boolean>()
|
||||||
val isLoading: LiveData<Boolean> = _isLoading
|
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 {
|
init {
|
||||||
Log.d(TAG, "HomeViewModel initialized")
|
Log.d(TAG, "HomeViewModel initialized")
|
||||||
loadUserData()
|
loadUserData()
|
||||||
checkOutstandingBills()
|
checkOutstandingBills()
|
||||||
|
loadCustomerSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadUserData() {
|
private fun loadUserData() {
|
||||||
val userName = secureStorage.getUserName() ?: "User"
|
val activeAccount = secureStorage.getActiveAccount()
|
||||||
|
val userName = activeAccount?.name ?: "User"
|
||||||
Log.d(TAG, "Loading user data: $userName")
|
Log.d(TAG, "Loading user data: $userName")
|
||||||
_welcomeText.value = "Welcome\n$userName"
|
_welcomeText.value = "Welcome\n$userName"
|
||||||
}
|
}
|
||||||
@@ -52,14 +87,14 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading.value = true
|
_isLoadingBills.value = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Log.d(TAG, "Making API call to check outstanding bills")
|
Log.d(TAG, "Making API call to check outstanding bills")
|
||||||
when (val result = apiService.getOutstandingBills("connect.sid=$cookie")) {
|
when (val result = apiService.getOutstandingBills("connect.sid=$cookie")) {
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
Log.d(TAG, "Bills check successful: ${result.data.size} bills found")
|
Log.d(TAG, "Bills check successful: ${result.data.size} bills found")
|
||||||
_isLoading.value = false
|
_isLoadingBills.value = false
|
||||||
if (result.data.isEmpty()) {
|
if (result.data.isEmpty()) {
|
||||||
_billsStatus.value = "You don't have any pending bills left"
|
_billsStatus.value = "You don't have any pending bills left"
|
||||||
} else {
|
} else {
|
||||||
@@ -68,10 +103,117 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
is ApiResult.Error -> {
|
is ApiResult.Error -> {
|
||||||
Log.d(TAG, "Bills check failed: ${result.message}")
|
Log.d(TAG, "Bills check failed: ${result.message}")
|
||||||
_isLoading.value = false
|
_isLoadingBills.value = false
|
||||||
_billsStatus.value = "Unable to check bills status"
|
_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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="20dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<!-- Welcome Section -->
|
<!-- Welcome Section -->
|
||||||
<TextView
|
<TextView
|
||||||
@@ -64,16 +64,237 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Placeholder for future features -->
|
<!-- Subscriptions Overview Card -->
|
||||||
<TextView
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/card_subscriptions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="More features coming soon..."
|
android:layout_marginBottom="24dp"
|
||||||
android:textSize="14sp"
|
app:cardCornerRadius="12dp"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
app:cardElevation="2dp">
|
||||||
android:gravity="center"
|
|
||||||
android:layout_marginTop="32dp" />
|
<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>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- Material Design default colors -->
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
<color name="purple_500">#FF6200EE</color>
|
||||||
<color name="purple_700">#FF3700B3</color>
|
<color name="purple_700">#FF3700B3</color>
|
||||||
@@ -7,4 +8,10 @@
|
|||||||
<color name="teal_700">#FF018786</color>
|
<color name="teal_700">#FF018786</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</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