diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fd2922f..2c6843f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/data/Models.kt b/app/src/main/java/sh/sar/gridflow/data/Models.kt
index a10d49a..b943047 100644
--- a/app/src/main/java/sh/sar/gridflow/data/Models.kt
+++ b/app/src/main/java/sh/sar/gridflow/data/Models.kt
@@ -1,5 +1,7 @@
package sh.sar.gridflow.data
+import java.io.Serializable
+
data class LoginRequest(
val mobile: String,
val password: String
@@ -80,7 +82,7 @@ data class MainCategory(
val id: Int,
val name: String,
val code: String
-)
+) : Serializable
data class Customer(
val id: Long,
@@ -88,24 +90,24 @@ data class Customer(
val accountNumber: String,
val phone: String,
val type: String
-)
+) : Serializable
data class SubscriptionAddress(
val id: Long,
val type: String,
val property: Property?
-)
+) : Serializable
data class Property(
val id: Long,
val name: String,
val street: Street?
-)
+) : Serializable
data class Street(
val id: Long,
val name: String
-)
+) : Serializable
data class LastBill(
val id: Long,
@@ -158,3 +160,256 @@ data class BandRate(
val band: String,
val rate: String
)
+
+// Bill History Models
+data class Bill(
+ val id: Long,
+ val billNumber: String,
+ val billAmount: String,
+ val carryForwardBalance: String,
+ val discount: String?,
+ val tax: String?,
+ val billedUnits: String,
+ val dueDate: String,
+ val billDate: String,
+ val billYear: Int,
+ val billMonth: Int,
+ val deliveredDate: String?,
+ val warnDate: String?,
+ val warnDueDate: String?,
+ val status: String, // "paid" or "unpaid"
+ val type: String,
+ val isDelivered: Boolean,
+ val oldId: String?,
+ val oldBranchId: String?,
+ val billDuration: Int,
+ val fineStatus: String,
+ val referenceNo: Int,
+ val details: String?,
+ val groupUuid: String,
+ val publicId: String,
+ val createdAt: String,
+ val updatedAt: String,
+ val customerId: Long,
+ val categoryId: Int,
+ val subscriptionAddressId: Long,
+ val billingAddressId: Long,
+ val subscriptionId: Long,
+ val consumerId: Long,
+ val readingEventId: Long,
+ val branchId: Int,
+ val activeBillCancel: String?,
+ val billingAddress: BillingAddress,
+ val subscription: BillSubscription,
+ val subscriptionAddress: SubscriptionAddress,
+ val customer: Customer,
+ val consumer: Customer,
+ val category: BillCategory,
+ val miscCustomerDetail: String?,
+ val billUnits: List,
+ val billCharges: List,
+ val billedReadings: List,
+ val outstandingBillAmounts: List,
+ val billFineViews: List,
+ val billables: List,
+ val outstandingBills: List,
+ val outstandingBillTotal: Double,
+ val meter: BilledReading,
+ val measurementUnit: String,
+ val billableFuelDiscount: Billable?,
+ val billableFuelSurcharge: Billable?,
+ val billableTariff: Billable?,
+ val billableItem: List,
+ val billableMisc: List,
+ val billDiscount: List,
+ val otherBillables: List,
+ val billableFine: List,
+ val unitMeterUnits: List,
+ val normalMeterUnits: List,
+ val billPaymentDetails: BillPaymentDetails?,
+ val paidDate: String?,
+ val alias: String
+) : Serializable
+
+data class BillingAddress(
+ val id: Long,
+ val type: String,
+ val startAt: String,
+ val endAt: String?,
+ val oldId: String?,
+ val oldServiceId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val propertyId: Long,
+ val subscriptionId: Long,
+ val apartmentId: String?,
+ val property: Property
+) : Serializable
+
+data class BillSubscription(
+ val id: Long,
+ val subscriptionNumber: String,
+ val isTemporary: Boolean,
+ val readingOrder: Int,
+ val deliveryOrder: Int,
+ val groupUuid: String,
+ val email: String?,
+ val status: String,
+ val oldId: String?,
+ val oldServiceId: String?,
+ val oldBranchId: String?,
+ val carryForwardBalance: String,
+ val depositBalance: String?,
+ val hasSmartMeter: Boolean,
+ val subscribedAt: String,
+ val createdAt: String,
+ val updatedAt: String,
+ val areaId: Int,
+ val branchId: Int,
+ val categoryId: Int,
+ val customerId: Long,
+ val requestId: String?,
+ val serviceId: Int,
+ val branch: Branch,
+ val service: Service
+) : Serializable
+
+data class Branch(
+ val id: Int,
+ val name: String,
+ val code: String,
+ val phone: String,
+ val inquiryContact: String,
+ val powerFailureContact: String,
+ val oldId: String?,
+ val oldServiceId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val propertyId: String?,
+ val apartmentId: String?,
+ val branchGroupId: Int
+) : Serializable
+
+data class Service(
+ val id: Int,
+ val name: String,
+ val connectionFee: String,
+ val oldId: String?,
+ val oldServiceId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String
+) : Serializable
+
+data class BillCategory(
+ val id: Int,
+ val name: String,
+ val code: String,
+ val gracePeriod: Int,
+ val noticeDuration: Int,
+ val periodicalChargeType: String,
+ val periodicalCharge: String,
+ val isFuelSurchargeSubsidized: Boolean,
+ val isFuelSurchargeApplied: Boolean,
+ val fineMethod: String,
+ val fineValue: String,
+ val fineMovement: String,
+ val fineInterval: Int,
+ val fineTo: String,
+ val oldId: String?,
+ val oldServiceId: String?,
+ val oldBranchId: String?,
+ val mainCategoryId: Int,
+ val serviceId: Int,
+ val isTemporary: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val mainCategory: MainCategory
+) : Serializable
+
+data class BillUnit(
+ val id: Long,
+ val type: String,
+ val isDeduct: Boolean,
+ val units: String,
+ val oldId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val billId: Long,
+ val billableTariffId: Long,
+ val meterUnitId: Long
+) : Serializable
+
+data class BillCharge(
+ val id: Long,
+ val bandMin: String,
+ val bandMax: String,
+ val duration: Int,
+ val rate: String,
+ val units: String,
+ val oldId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val deletedAt: String?,
+ val billId: Long,
+ val billableTariffId: Long,
+ val tariffId: Long
+) : Serializable
+
+data class BilledReading(
+ val id: Long,
+ val currentReading: String,
+ val previousReading: String,
+ val currentUnits: String,
+ val previousUnits: String,
+ val meterType: String,
+ val readingType: String,
+ val meterNo: String,
+ val currentReadingDate: String,
+ val previousReadingDate: String,
+ val measurementUnit: String,
+ val createdAt: String,
+ val updatedAt: String,
+ val billId: Long,
+ val currentReadingId: Long,
+ val previousReadingId: Long,
+ val subscriptionItemId: Long
+) : Serializable
+
+data class Billable(
+ val id: Long,
+ val type: String,
+ val amount: String,
+ val oldId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val billId: Long,
+ val subscriptionId: Long,
+ val billableItems: List,
+ val billableMiscs: List
+) : Serializable
+
+data class BillableItem(
+ val id: Long,
+ val quantity: String,
+ val description: String,
+ val oldId: String?,
+ val oldBranchId: String?,
+ val createdAt: String,
+ val updatedAt: String,
+ val billableId: Long,
+ val subscriptionItemId: Long
+) : Serializable
+
+data class BillPaymentDetails(
+ val billId: Long,
+ val advancePaid: String,
+ val paidAmount: String,
+ val miscFine: String,
+ val pendingAmount: String
+) : Serializable
diff --git a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
index ee30bc2..437e9e6 100644
--- a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
+++ b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt
@@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
import sh.sar.gridflow.data.BandRatesResponse
+import sh.sar.gridflow.data.Bill
import sh.sar.gridflow.data.CustomerSubscription
import sh.sar.gridflow.data.ErrorResponse
import sh.sar.gridflow.data.ForgotPasswordRequest
@@ -371,6 +372,87 @@ class FenakaApiService {
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
+
+ suspend fun getBillHistory(cookie: String): ApiResult> = withContext(Dispatchers.IO) {
+ Log.d(TAG, "Fetching bill history")
+
+ val cookieHeader = "connect.sid=$cookie"
+
+ val request = Request.Builder()
+ .url("$BASE_URL/api/customer-bills-detail")
+ .get()
+ .header("Authorization", "Bearer $BEARER_TOKEN")
+ .header("Content-Type", "application/json")
+ .header("Cookie", cookieHeader)
+ .header("Host", "api.fenaka.mv")
+ .header("User-Agent", "Dart/3.5 (dart:io)")
+ .build()
+
+ try {
+ val response = client.newCall(request).execute()
+ Log.d(TAG, "Bill history response code: ${response.code}")
+
+ when (response.code) {
+ 200 -> {
+ val responseBody = response.body?.string() ?: "[]"
+ Log.d(TAG, "Bill history response length: ${responseBody.length}")
+ val bills = gson.fromJson(responseBody, Array::class.java).toList()
+ Log.d(TAG, "Parsed ${bills.size} bills successfully")
+ ApiResult.Success(bills, null)
+ }
+ else -> {
+ Log.d(TAG, "Failed to fetch bill history: ${response.code}")
+ ApiResult.Error("Failed to fetch bill history", response.code)
+ }
+ }
+ } catch (e: IOException) {
+ Log.e(TAG, "Network error during bill history fetch", e)
+ ApiResult.Error("Network error: ${e.message}", -1)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unexpected error during bill history fetch", e)
+ ApiResult.Error("Unexpected error: ${e.message}", -1)
+ }
+ }
+
+ suspend fun downloadBillPdf(billId: Long, cookie: String): ApiResult = withContext(Dispatchers.IO) {
+ Log.d(TAG, "Downloading PDF for bill: $billId")
+
+ val cookieHeader = "connect.sid=$cookie"
+
+ val request = Request.Builder()
+ .url("$BASE_URL/saiph/bills/$billId/pdf")
+ .get()
+ .header("Authorization", "Bearer $BEARER_TOKEN")
+ .header("Cookie", cookieHeader)
+ .header("Host", "api.fenaka.mv")
+ .header("User-Agent", "Dart/3.5 (dart:io)")
+ .build()
+
+ try {
+ val response = client.newCall(request).execute()
+ Log.d(TAG, "PDF download response code: ${response.code}")
+
+ when (response.code) {
+ 200 -> {
+ val responseBody = response.body?.string() ?: ""
+ Log.d(TAG, "PDF download response length: ${responseBody.length}")
+ // Remove quotes from beginning and end if present
+ val base64Content = responseBody.trim().removeSurrounding("\"")
+ ApiResult.Success(base64Content, null)
+ }
+ else -> {
+ Log.d(TAG, "Failed to download PDF: ${response.code}")
+ ApiResult.Error("Failed to download PDF", response.code)
+ }
+ }
+ } catch (e: IOException) {
+ Log.e(TAG, "Network error during PDF download", e)
+ ApiResult.Error("Network error: ${e.message}", -1)
+ } catch (e: Exception) {
+ Log.e(TAG, "Unexpected error during PDF download", e)
+ ApiResult.Error("Unexpected error: ${e.message}", -1)
+ }
+ }
}
sealed class ApiResult {
diff --git a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillCardAdapter.kt b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillCardAdapter.kt
new file mode 100644
index 0000000..dda053b
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillCardAdapter.kt
@@ -0,0 +1,106 @@
+package sh.sar.gridflow.ui.billhistory
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
+import sh.sar.gridflow.R
+import sh.sar.gridflow.data.Bill
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+class BillCardAdapter(
+ private val onBillClick: (Bill) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var bills: List = emptyList()
+
+ fun updateBills(newBills: List) {
+ bills = newBills
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BillViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_bill_card, parent, false)
+ return BillViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: BillViewHolder, position: Int) {
+ holder.bind(bills[position])
+ }
+
+ override fun getItemCount(): Int = bills.size
+
+ inner class BillViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val billDate: TextView = itemView.findViewById(R.id.billDate)
+ private val billAmount: TextView = itemView.findViewById(R.id.billAmount)
+ private val billNumber: TextView = itemView.findViewById(R.id.billNumber)
+ private val paymentStatus: TextView = itemView.findViewById(R.id.paymentStatus)
+ private val payNowButton: MaterialButton = itemView.findViewById(R.id.payNowButton)
+
+ fun bind(bill: Bill) {
+ // Format bill date
+ billDate.text = formatBillDate(bill)
+
+ // Set bill amount
+ billAmount.text = "MVR ${bill.billAmount}"
+
+ // Set bill number
+ billNumber.text = "Bill #${bill.billNumber}"
+
+ // Set payment status
+ if (bill.status.equals("paid", ignoreCase = true)) {
+ paymentStatus.visibility = View.VISIBLE
+ payNowButton.visibility = View.GONE
+
+ val paidDate = formatPaidDate(bill.paidDate)
+ paymentStatus.text = "Paid on $paidDate"
+ paymentStatus.setTextColor(ContextCompat.getColor(itemView.context, android.R.color.holo_green_dark))
+ } else {
+ paymentStatus.visibility = View.GONE
+ payNowButton.visibility = View.VISIBLE
+
+ payNowButton.setOnClickListener {
+ Toast.makeText(itemView.context, "Coming soon", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ // Set click listener for the card
+ itemView.setOnClickListener {
+ onBillClick(bill)
+ }
+ }
+
+ private fun formatBillDate(bill: Bill): String {
+ return try {
+ val calendar = Calendar.getInstance()
+ calendar.set(bill.billYear, bill.billMonth - 1, 1) // Month is 0-based in Calendar
+ val monthName = calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) ?: ""
+ "$monthName ${bill.billYear}"
+ } catch (e: Exception) {
+ "${bill.billMonth}/${bill.billYear}"
+ }
+ }
+
+ private fun formatPaidDate(paidDate: String?): String {
+ return if (paidDate != null) {
+ try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ val outputFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
+ val date = inputFormat.parse(paidDate)
+ date?.let { outputFormat.format(it) } ?: paidDate
+ } catch (e: Exception) {
+ paidDate
+ }
+ } else {
+ "Unknown"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillDetailsActivity.kt b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillDetailsActivity.kt
new file mode 100644
index 0000000..839f0c1
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillDetailsActivity.kt
@@ -0,0 +1,393 @@
+package sh.sar.gridflow.ui.billhistory
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.Environment
+import android.util.Base64
+import android.util.Log
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.lifecycle.ViewModelProvider
+import sh.sar.gridflow.R
+import sh.sar.gridflow.data.Bill
+import sh.sar.gridflow.databinding.ActivityBillDetailsBinding
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class BillDetailsActivity : AppCompatActivity() {
+
+ companion object {
+ private const val TAG = "BillDetailsActivity"
+ const val EXTRA_BILL_ID = "bill_id"
+ const val EXTRA_BILL_DATA = "bill_data"
+ }
+
+ private lateinit var binding: ActivityBillDetailsBinding
+ private lateinit var viewModel: BillHistoryViewModel
+ private var currentBill: Bill? = null
+ private var currentPdfFile: File? = null
+ private var currentPdfAction: PdfAction? = null
+
+ enum class PdfAction {
+ SHARE, DOWNLOAD, OPEN
+ }
+
+ private val requestStoragePermission = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ if (isGranted) {
+ currentBill?.let { bill ->
+ if (currentPdfFile != null) {
+ downloadPdfToDownloads()
+ } else {
+ viewModel.downloadPdf(bill.id)
+ }
+ }
+ } else {
+ Toast.makeText(this, "Storage permission required to download files", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityBillDetailsBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ viewModel = ViewModelProvider(this)[BillHistoryViewModel::class.java]
+
+ setupToolbar()
+ setupObservers()
+ setupClickListeners()
+
+ // Get bill data from intent
+ val billData = intent.getSerializableExtra(EXTRA_BILL_DATA) as? Bill
+ val billId = intent.getLongExtra(EXTRA_BILL_ID, -1L)
+
+ if (billData != null) {
+ currentBill = billData
+ // Show main content immediately since we have bill data
+ binding.mainContent.visibility = View.VISIBLE
+ binding.progressBar.visibility = View.GONE
+ binding.errorMessage.visibility = View.GONE
+ populateBillDetails(billData)
+ } else if (billId != -1L) {
+ // Load bill data by ID if needed (fallback, shouldn't happen normally)
+ Log.d(TAG, "Loading bill details for ID: $billId")
+ } else {
+ Toast.makeText(this, "No bill data found", Toast.LENGTH_SHORT).show()
+ finish()
+ }
+ }
+
+ private fun setupToolbar() {
+ binding.toolbar.setNavigationOnClickListener {
+ finish()
+ }
+ }
+
+ private fun setupObservers() {
+ // Only observe PDF data for download/share functionality
+ viewModel.pdfBase64.observe(this) { base64Data ->
+ if (base64Data != null) {
+ handlePdfData(base64Data)
+ viewModel.clearPdf()
+ }
+ }
+
+ // Observe loading state for PDF downloads
+ viewModel.isLoading.observe(this) { isLoading ->
+ if (isLoading) {
+ showPdfLoading()
+ } else {
+ hidePdfLoading()
+ }
+ }
+ }
+
+ private fun setupClickListeners() {
+ binding.shareButton.setOnClickListener {
+ currentBill?.let { bill ->
+ currentPdfAction = PdfAction.SHARE
+ if (currentPdfFile != null) {
+ sharePdf(currentPdfFile!!)
+ } else {
+ viewModel.downloadPdf(bill.id)
+ }
+ }
+ }
+
+ binding.downloadButton.setOnClickListener {
+ currentBill?.let { bill ->
+ currentPdfAction = PdfAction.DOWNLOAD
+ if (currentPdfFile != null) {
+ downloadPdfToDownloads()
+ } else {
+ if (hasStoragePermission()) {
+ viewModel.downloadPdf(bill.id)
+ } else {
+ requestStoragePermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ }
+ }
+ }
+
+ binding.openButton.setOnClickListener {
+ currentBill?.let { bill ->
+ currentPdfAction = PdfAction.OPEN
+ if (currentPdfFile != null) {
+ openPdf(currentPdfFile!!)
+ } else {
+ viewModel.downloadPdf(bill.id)
+ }
+ }
+ }
+
+ binding.payNowButton.setOnClickListener {
+ Toast.makeText(this, "Coming soon", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun populateBillDetails(bill: Bill) {
+ // Card 1: Bill Information
+ binding.billNumber.text = bill.billNumber
+ binding.billTotal.text = "MVR ${bill.billAmount}"
+
+ // Set payment status
+ if (bill.status.equals("paid", ignoreCase = true)) {
+ binding.paidDate.visibility = View.VISIBLE
+ binding.payNowButton.visibility = View.GONE
+ binding.paidDate.text = formatPaidDate(bill.paidDate)
+ binding.paidDate.setTextColor(ContextCompat.getColor(this, android.R.color.holo_green_dark))
+
+ // Set card color based on service type
+ val colorRes = when (bill.subscription.serviceId) {
+ 1 -> android.R.color.holo_orange_light
+ 2 -> android.R.color.holo_blue_light
+ else -> android.R.color.holo_orange_light
+ }
+ binding.billInfoCard.setCardBackgroundColor(ContextCompat.getColor(this, colorRes))
+ } else {
+ binding.paidDate.visibility = View.GONE
+ binding.payNowButton.visibility = View.VISIBLE
+ binding.billInfoCard.setCardBackgroundColor(ContextCompat.getColor(this, android.R.color.holo_red_light))
+ }
+
+ // Card 2: Subscription Details
+ binding.subscriptionNo.text = bill.subscription.subscriptionNumber
+ binding.accountNo.text = bill.customer.accountNumber
+ binding.branch.text = bill.subscription.branch.name
+ binding.meterNo.text = bill.meter.meterNo
+ binding.tariffCode.text = bill.category.code
+ binding.billDate.text = formatBillDate(bill.billDate)
+
+ // Card 3: Meter Readings
+ // Current month row
+ binding.readingValue.text = bill.meter.currentReading
+ binding.usageValue.text = "${bill.meter.currentUnits} ${bill.measurementUnit}"
+
+ // Previous month row
+ binding.previousReadingValue.text = bill.meter.previousReading
+ binding.previousUsageValue.text = "${bill.meter.previousUnits} ${bill.measurementUnit}"
+
+ // Card 4: Monthly Statement (Bill Charges)
+ populateCharges(bill)
+ }
+
+ private fun populateCharges(bill: Bill) {
+ val container = binding.chargesContainer
+ container.removeAllViews()
+
+ // Get the primary text color from theme
+ val textColor = getThemeTextColor()
+
+ bill.billCharges.forEach { charge ->
+ val chargeRow = LinearLayout(this).apply {
+ orientation = LinearLayout.HORIZONTAL
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ).apply {
+ bottomMargin = 8
+ }
+ setPadding(8, 8, 8, 8)
+ }
+
+ val qtyText = TextView(this).apply {
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
+ text = charge.units
+ textSize = 12f
+ setTextColor(textColor)
+ }
+
+ val rateText = TextView(this).apply {
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
+ text = "MVR ${charge.rate}"
+ textSize = 12f
+ gravity = android.view.Gravity.CENTER
+ setTextColor(textColor)
+ }
+
+ val amountText = TextView(this).apply {
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
+ val amount = charge.units.toFloatOrNull()?.times(charge.rate.toFloatOrNull() ?: 0f) ?: 0f
+ text = "MVR %.2f".format(amount)
+ textSize = 12f
+ gravity = android.view.Gravity.END
+ setTextColor(textColor)
+ }
+
+ chargeRow.addView(qtyText)
+ chargeRow.addView(rateText)
+ chargeRow.addView(amountText)
+
+ container.addView(chargeRow)
+ }
+ }
+
+ private fun getThemeTextColor(): Int {
+ val typedValue = android.util.TypedValue()
+ theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
+ return ContextCompat.getColor(this, typedValue.resourceId)
+ }
+
+ private fun showPdfLoading() {
+ binding.pdfLoadingSpinner.visibility = View.VISIBLE
+ binding.shareButton.visibility = View.GONE
+ binding.downloadButton.visibility = View.GONE
+ binding.openButton.visibility = View.GONE
+ }
+
+ private fun hidePdfLoading() {
+ binding.pdfLoadingSpinner.visibility = View.GONE
+ binding.shareButton.visibility = View.VISIBLE
+ binding.downloadButton.visibility = View.VISIBLE
+ binding.openButton.visibility = View.VISIBLE
+ }
+
+ private fun handlePdfData(base64Data: String) {
+ try {
+ val pdfBytes = Base64.decode(base64Data, Base64.DEFAULT)
+ val fileName = "${currentBill?.billNumber ?: "bill"}.pdf"
+
+ // Save to app's internal cache first
+ val cacheFile = File(cacheDir, fileName)
+ FileOutputStream(cacheFile).use { it.write(pdfBytes) }
+ currentPdfFile = cacheFile
+
+ // Perform the appropriate action based on what button was pressed
+ when (currentPdfAction) {
+ PdfAction.SHARE -> sharePdf(cacheFile)
+ PdfAction.DOWNLOAD -> downloadPdfToDownloads()
+ PdfAction.OPEN -> openPdf(cacheFile)
+ null -> {
+ // Fallback behavior (shouldn't happen)
+ openPdf(cacheFile)
+ }
+ }
+
+ } catch (e: Exception) {
+ Log.e(TAG, "Error handling PDF data", e)
+ Toast.makeText(this, "Error processing PDF: ${e.message}", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun downloadPdfToDownloads() {
+ currentPdfFile?.let { cacheFile ->
+ try {
+ val downloadsDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "GridFlow")
+ if (!downloadsDir.exists()) {
+ downloadsDir.mkdirs()
+ }
+
+ val fileName = "${currentBill?.billNumber ?: "bill"}.pdf"
+ val downloadFile = File(downloadsDir, fileName)
+
+ cacheFile.copyTo(downloadFile, overwrite = true)
+ Toast.makeText(this, "PDF saved to Downloads/GridFlow/$fileName", Toast.LENGTH_LONG).show()
+
+ } catch (e: IOException) {
+ Log.e(TAG, "Error downloading PDF", e)
+ Toast.makeText(this, "Error downloading PDF: ${e.message}", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+
+ private fun sharePdf(file: File) {
+ try {
+ val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "application/pdf"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ startActivity(Intent.createChooser(intent, "Share PDF"))
+ } catch (e: Exception) {
+ Log.e(TAG, "Error sharing PDF", e)
+ Toast.makeText(this, "Error sharing PDF: ${e.message}", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun openPdf(file: File) {
+ try {
+ val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ setDataAndType(uri, "application/pdf")
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+
+ if (intent.resolveActivity(packageManager) != null) {
+ startActivity(intent)
+ } else {
+ Toast.makeText(this, "No PDF viewer app found", Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error opening PDF", e)
+ Toast.makeText(this, "Error opening PDF: ${e.message}", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun hasStoragePermission(): Boolean {
+ return ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun formatPaidDate(paidDate: String?): String {
+ return if (paidDate != null) {
+ try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ val outputFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
+ val date = inputFormat.parse(paidDate)
+ date?.let { outputFormat.format(it) } ?: paidDate
+ } catch (e: Exception) {
+ paidDate
+ }
+ } else {
+ "Unknown"
+ }
+ }
+
+ private fun formatBillDate(billDate: String): String {
+ return try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ val outputFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault())
+ val date = inputFormat.parse(billDate)
+ date?.let { outputFormat.format(it) } ?: billDate
+ } catch (e: Exception) {
+ billDate
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryFragment.kt b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryFragment.kt
index 91a10cb..84f3e81 100644
--- a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryFragment.kt
+++ b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryFragment.kt
@@ -1,36 +1,146 @@
package sh.sar.gridflow.ui.billhistory
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.gridflow.databinding.FragmentBillHistoryBinding
+import sh.sar.gridflow.data.Bill
class BillHistoryFragment : Fragment() {
private var _binding: FragmentBillHistoryBinding? = null
private val binding get() = _binding!!
+
+ private lateinit var billHistoryViewModel: BillHistoryViewModel
+ private lateinit var subscriptionAdapter: SubscriptionCardAdapter
+ private lateinit var billAdapter: BillCardAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- val billHistoryViewModel =
- ViewModelProvider(this).get(BillHistoryViewModel::class.java)
-
+ billHistoryViewModel = ViewModelProvider(this)[BillHistoryViewModel::class.java]
_binding = FragmentBillHistoryBinding.inflate(inflater, container, false)
val root: View = binding.root
- val textView = binding.textBillHistory
- billHistoryViewModel.text.observe(viewLifecycleOwner) {
- textView.text = it
- }
+ setupRecyclerViews()
+ setupObservers()
+ setupClickListeners()
return root
}
+
+ private fun setupRecyclerViews() {
+ // Setup subscription RecyclerView
+ subscriptionAdapter = SubscriptionCardAdapter { subscription ->
+ billHistoryViewModel.selectSubscription(subscription)
+ subscriptionAdapter.setSelectedSubscription(subscription.id)
+ showBillsForSubscription(subscription.subscription.id)
+ }
+
+ binding.subscriptionsRecyclerView.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = subscriptionAdapter
+ }
+
+ // Setup bills RecyclerView
+ billAdapter = BillCardAdapter { bill ->
+ billHistoryViewModel.selectBill(bill)
+ navigateToBillDetails(bill)
+ }
+
+ binding.billsRecyclerView.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = billAdapter
+ }
+ }
+
+ private fun setupObservers() {
+ // Loading state
+ billHistoryViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
+ binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
+ binding.subscriptionsContent.visibility = if (isLoading) View.GONE else View.VISIBLE
+ }
+
+ // Error handling
+ billHistoryViewModel.errorMessage.observe(viewLifecycleOwner) { error ->
+ if (error != null) {
+ binding.errorMessage.text = error
+ binding.errorMessage.visibility = View.VISIBLE
+ binding.cardSubscriptions.visibility = View.GONE
+ binding.cardBills.visibility = View.GONE
+ } else {
+ binding.errorMessage.visibility = View.GONE
+ binding.cardSubscriptions.visibility = View.VISIBLE
+ }
+ }
+
+ // Subscriptions
+ billHistoryViewModel.subscriptions.observe(viewLifecycleOwner) { subscriptions ->
+ subscriptionAdapter.updateSubscriptions(subscriptions)
+
+ // Auto-select first subscription if available and none is currently selected
+ if (subscriptions.isNotEmpty() && billHistoryViewModel.selectedSubscription.value == null) {
+ val firstSubscription = subscriptions.first()
+ billHistoryViewModel.selectSubscription(firstSubscription)
+ subscriptionAdapter.setSelectedSubscription(firstSubscription.id)
+ showBillsForSubscription(firstSubscription.subscription.id)
+ }
+ }
+
+ // Bills
+ billHistoryViewModel.bills.observe(viewLifecycleOwner) { bills ->
+ // Bills will be filtered per subscription when needed
+ }
+
+ // Selected subscription
+ billHistoryViewModel.selectedSubscription.observe(viewLifecycleOwner) { subscription ->
+ if (subscription != null) {
+ binding.billsTitle.text = "Bills"
+ }
+ }
+ }
+
+ private fun setupClickListeners() {
+ binding.errorMessage.setOnClickListener {
+ billHistoryViewModel.clearError()
+ }
+ }
+
+ private fun showBillsForSubscription(subscriptionId: Long) {
+ val billsForSubscription = billHistoryViewModel.getBillsForSubscription(subscriptionId)
+
+ if (billsForSubscription.isEmpty()) {
+ binding.billsRecyclerView.visibility = View.GONE
+ binding.noBillsMessage.visibility = View.VISIBLE
+ } else {
+ binding.billsRecyclerView.visibility = View.VISIBLE
+ binding.noBillsMessage.visibility = View.GONE
+ billAdapter.updateBills(billsForSubscription)
+ }
+
+ // Show bills card below subscriptions
+ binding.cardBills.visibility = View.VISIBLE
+ }
+
+ private fun showSubscriptionSelection() {
+ // Hide bills card when no subscription selected
+ binding.cardBills.visibility = View.GONE
+ }
+
+ private fun navigateToBillDetails(bill: Bill) {
+ val intent = Intent(context, BillDetailsActivity::class.java).apply {
+ putExtra(BillDetailsActivity.EXTRA_BILL_DATA, bill)
+ }
+ startActivity(intent)
+ }
override fun onDestroyView() {
super.onDestroyView()
diff --git a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryViewModel.kt b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryViewModel.kt
index 1e7f075..9a08396 100644
--- a/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryViewModel.kt
+++ b/app/src/main/java/sh/sar/gridflow/ui/billhistory/BillHistoryViewModel.kt
@@ -1,13 +1,198 @@
package sh.sar.gridflow.ui.billhistory
+import android.app.Application
+import android.util.Log
+import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import sh.sar.gridflow.data.Bill
+import sh.sar.gridflow.data.CustomerSubscription
+import sh.sar.gridflow.data.ServiceCategory
+import sh.sar.gridflow.data.SubscriptionDetails
+import sh.sar.gridflow.network.ApiResult
+import sh.sar.gridflow.network.FenakaApiService
+import sh.sar.gridflow.utils.SecureStorage
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
-class BillHistoryViewModel : ViewModel() {
-
- private val _text = MutableLiveData().apply {
- value = "Bill History\n\nComing soon..."
+class BillHistoryViewModel(application: Application) : AndroidViewModel(application) {
+
+ companion object {
+ private const val TAG = "BillHistoryViewModel"
+ }
+
+ private val secureStorage = SecureStorage(application)
+ private val apiService = FenakaApiService()
+
+ private val _isLoading = MutableLiveData()
+ val isLoading: LiveData = _isLoading
+
+ private val _errorMessage = MutableLiveData()
+ val errorMessage: LiveData = _errorMessage
+
+ private val _subscriptions = MutableLiveData>()
+ val subscriptions: LiveData> = _subscriptions
+
+ private val _bills = MutableLiveData>()
+ val bills: LiveData> = _bills
+
+ private val _selectedSubscription = MutableLiveData()
+ val selectedSubscription: LiveData = _selectedSubscription
+
+ private val _selectedBill = MutableLiveData()
+ val selectedBill: LiveData = _selectedBill
+
+ private val _pdfBase64 = MutableLiveData()
+ val pdfBase64: LiveData = _pdfBase64
+
+ init {
+ loadBills()
+ }
+
+
+ private fun loadBills() {
+ viewModelScope.launch {
+ _isLoading.value = true
+ _errorMessage.value = null
+
+ val cookie = secureStorage.getCookie()
+ if (cookie == null) {
+ _errorMessage.value = "Please login first"
+ _isLoading.value = false
+ return@launch
+ }
+
+ when (val result = apiService.getBillHistory(cookie)) {
+ is ApiResult.Success -> {
+ Log.d(TAG, "Loaded ${result.data.size} bills")
+ // Sort bills by date (latest first)
+ val sortedBills = result.data.sortedByDescending {
+ try {
+ val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
+ format.parse(it.billDate)?.time ?: 0L
+ } catch (e: Exception) {
+ // Fallback to year/month sorting
+ (it.billYear * 12 + it.billMonth).toLong()
+ }
+ }
+ _bills.value = sortedBills
+
+ // Extract unique subscriptions from bills
+ val uniqueSubscriptions = sortedBills.groupBy { it.subscriptionId }.map { (_, bills) ->
+ val firstBill = bills.first()
+ // Create a CustomerSubscription-like object from bill data
+ CustomerSubscription(
+ id = firstBill.subscriptionId,
+ name = firstBill.alias,
+ subscriptionId = firstBill.subscriptionId,
+ createdAt = firstBill.createdAt,
+ deletedAt = null,
+ customerId = firstBill.customerId,
+ subscription = SubscriptionDetails(
+ id = firstBill.subscription.id,
+ subscriptionNumber = firstBill.subscription.subscriptionNumber,
+ status = firstBill.subscription.status,
+ serviceId = firstBill.subscription.serviceId,
+ hasSmartMeter = firstBill.subscription.hasSmartMeter,
+ category = ServiceCategory(
+ id = firstBill.category.id,
+ name = firstBill.category.name,
+ code = firstBill.category.code,
+ mainCategory = firstBill.category.mainCategory
+ ),
+ customer = firstBill.customer,
+ lastBill = null,
+ subscriptionAddress = firstBill.subscriptionAddress
+ )
+ )
+ }
+ _subscriptions.value = uniqueSubscriptions
+ }
+ is ApiResult.Error -> {
+ Log.e(TAG, "Failed to load bills: ${result.message}")
+ _errorMessage.value = result.message
+ }
+ }
+ _isLoading.value = false
+ }
+ }
+
+ fun selectSubscription(subscription: CustomerSubscription) {
+ Log.d(TAG, "Selected subscription: ${subscription.subscription.subscriptionNumber}")
+ _selectedSubscription.value = subscription
+ }
+
+ fun selectBill(bill: Bill) {
+ Log.d(TAG, "Selected bill: ${bill.billNumber}")
+ _selectedBill.value = bill
+ }
+
+ fun downloadPdf(billId: Long) {
+ viewModelScope.launch {
+ _isLoading.value = true
+ _errorMessage.value = null
+
+ val cookie = secureStorage.getCookie()
+ if (cookie == null) {
+ _errorMessage.value = "Please login first"
+ _isLoading.value = false
+ return@launch
+ }
+
+ when (val result = apiService.downloadBillPdf(billId, cookie)) {
+ is ApiResult.Success -> {
+ Log.d(TAG, "Downloaded PDF for bill: $billId")
+ _pdfBase64.value = result.data
+ }
+ is ApiResult.Error -> {
+ Log.e(TAG, "Failed to download PDF: ${result.message}")
+ _errorMessage.value = result.message
+ }
+ }
+ _isLoading.value = false
+ }
+ }
+
+ fun clearError() {
+ _errorMessage.value = null
+ }
+
+ fun clearPdf() {
+ _pdfBase64.value = null
+ }
+
+ fun getBillsForSubscription(subscriptionId: Long): List {
+ return _bills.value?.filter { it.subscriptionId == subscriptionId } ?: emptyList()
+ }
+
+ fun formatBillDate(bill: Bill): String {
+ return try {
+ val calendar = Calendar.getInstance()
+ calendar.set(bill.billYear, bill.billMonth - 1, 1) // Month is 0-based in Calendar
+ val monthName = calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) ?: ""
+ "$monthName ${bill.billYear}"
+ } catch (e: Exception) {
+ "${bill.billMonth}/${bill.billYear}"
+ }
+ }
+
+ fun getServiceIcon(serviceId: Int): Int {
+ return when (serviceId) {
+ 1 -> sh.sar.gridflow.R.drawable.ic_electric_24 // Electricity
+ 2 -> sh.sar.gridflow.R.drawable.ic_water_24 // Water
+ else -> sh.sar.gridflow.R.drawable.ic_electric_24
+ }
+ }
+
+ fun getServiceColor(serviceId: Int): Int {
+ return when (serviceId) {
+ 1 -> android.R.color.holo_orange_light // Orange for electricity
+ 2 -> android.R.color.holo_blue_light // Blue for water
+ else -> android.R.color.holo_orange_light
+ }
}
- val text: LiveData = _text
}
diff --git a/app/src/main/java/sh/sar/gridflow/ui/billhistory/SubscriptionCardAdapter.kt b/app/src/main/java/sh/sar/gridflow/ui/billhistory/SubscriptionCardAdapter.kt
new file mode 100644
index 0000000..a0a10be
--- /dev/null
+++ b/app/src/main/java/sh/sar/gridflow/ui/billhistory/SubscriptionCardAdapter.kt
@@ -0,0 +1,128 @@
+package sh.sar.gridflow.ui.billhistory
+
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import sh.sar.gridflow.R
+import sh.sar.gridflow.data.CustomerSubscription
+
+class SubscriptionCardAdapter(
+ private val onSubscriptionClick: (CustomerSubscription) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var subscriptions: List = emptyList()
+ private var selectedSubscriptionId: Long? = null
+
+ fun updateSubscriptions(newSubscriptions: List) {
+ subscriptions = newSubscriptions
+ notifyDataSetChanged()
+ }
+
+ fun setSelectedSubscription(subscriptionId: Long?) {
+ val previousIndex = selectedSubscriptionId?.let { id ->
+ subscriptions.indexOfFirst { it.id == id }
+ }?.takeIf { it >= 0 }
+
+ val newIndex = subscriptionId?.let { id ->
+ subscriptions.indexOfFirst { it.id == id }
+ }?.takeIf { it >= 0 }
+
+ selectedSubscriptionId = subscriptionId
+
+ previousIndex?.let { notifyItemChanged(it) }
+ newIndex?.let { notifyItemChanged(it) }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_subscription_card, parent, false)
+ return SubscriptionViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
+ val subscription = subscriptions[position]
+ val isSelected = subscription.id == selectedSubscriptionId
+ holder.bind(subscription, isSelected)
+ }
+
+ override fun getItemCount(): Int = subscriptions.size
+
+ inner class SubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val serviceIcon: ImageView = itemView.findViewById(R.id.serviceIcon)
+ private val subscriptionName: TextView = itemView.findViewById(R.id.subscriptionName)
+ private val subscriptionDetails: TextView = itemView.findViewById(R.id.subscriptionDetails)
+
+ fun bind(subscription: CustomerSubscription, isSelected: Boolean) {
+ val subscriptionInfo = subscription.subscription
+
+ // Set subscription name from alias or property name
+ subscriptionName.text = subscription.name
+
+ // Set subscription details
+ val serviceName = when (subscriptionInfo.serviceId) {
+ 1 -> "Electricity"
+ 2 -> "Water"
+ else -> "Unknown Service"
+ }
+ val subscriptionNumber = subscriptionInfo.subscriptionNumber
+ subscriptionDetails.text = "$serviceName • #$subscriptionNumber"
+
+ // Set service icon
+ val iconRes = when (subscriptionInfo.serviceId) {
+ 1 -> R.drawable.ic_electric_24 // Electricity
+ 2 -> R.drawable.ic_water_24 // Water
+ else -> R.drawable.ic_electric_24
+ }
+ serviceIcon.setImageResource(iconRes)
+
+ // Set colors based on selection state and service type
+ if (isSelected) {
+ // Selected state - use service-specific colors
+ val backgroundColorRes = when (subscriptionInfo.serviceId) {
+ 1 -> R.color.electricity_selection_background // Light orange for electricity
+ 2 -> R.color.water_selection_background // Light blue for water
+ else -> R.color.electricity_selection_background
+ }
+ val iconColorRes = when (subscriptionInfo.serviceId) {
+ 1 -> R.color.electricity_selection_border // Dark orange for electricity
+ 2 -> R.color.water_selection_border // Dark blue for water
+ else -> R.color.electricity_selection_border
+ }
+
+ itemView.backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundColorRes)
+ serviceIcon.setColorFilter(ContextCompat.getColor(itemView.context, iconColorRes))
+ subscriptionName.setTextColor(ContextCompat.getColor(itemView.context, iconColorRes))
+ subscriptionDetails.setTextColor(ContextCompat.getColor(itemView.context, iconColorRes))
+ } else {
+ // Unselected state - use default colors
+ itemView.backgroundTintList = null
+ val iconColorRes = when (subscriptionInfo.serviceId) {
+ 1 -> android.R.color.holo_orange_light // Orange for electricity
+ 2 -> android.R.color.holo_blue_light // Blue for water
+ else -> android.R.color.holo_orange_light
+ }
+ serviceIcon.setColorFilter(ContextCompat.getColor(itemView.context, iconColorRes))
+
+ // Resolve theme attribute for text color
+ val typedValue = TypedValue()
+ itemView.context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
+ subscriptionName.setTextColor(ContextCompat.getColor(itemView.context, typedValue.resourceId))
+
+ // Set subscription details to secondary text color
+ val secondaryTypedValue = TypedValue()
+ itemView.context.theme.resolveAttribute(android.R.attr.textColorSecondary, secondaryTypedValue, true)
+ subscriptionDetails.setTextColor(ContextCompat.getColor(itemView.context, secondaryTypedValue.resourceId))
+ }
+
+ // Set click listener
+ itemView.setOnClickListener {
+ onSubscriptionClick(subscription)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/color/account_dropdown_background.xml b/app/src/main/res/color/account_dropdown_background.xml
index 7d75e13..89bbc15 100644
--- a/app/src/main/res/color/account_dropdown_background.xml
+++ b/app/src/main/res/color/account_dropdown_background.xml
@@ -1,5 +1,5 @@
-
-
+
+
diff --git a/app/src/main/res/drawable/ic_download_24.xml b/app/src/main/res/drawable/ic_download_24.xml
new file mode 100644
index 0000000..d6d0343
--- /dev/null
+++ b/app/src/main/res/drawable/ic_download_24.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_electric_24.xml b/app/src/main/res/drawable/ic_electric_24.xml
new file mode 100644
index 0000000..95181d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_electric_24.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_open_24.xml b/app/src/main/res/drawable/ic_open_24.xml
new file mode 100644
index 0000000..efbcf19
--- /dev/null
+++ b/app/src/main/res/drawable/ic_open_24.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_share_24.xml b/app/src/main/res/drawable/ic_share_24.xml
new file mode 100644
index 0000000..8b26dec
--- /dev/null
+++ b/app/src/main/res/drawable/ic_share_24.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_water_24.xml b/app/src/main/res/drawable/ic_water_24.xml
new file mode 100644
index 0000000..623c1d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_water_24.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_bill_details.xml b/app/src/main/res/layout/activity_bill_details.xml
new file mode 100644
index 0000000..65388f0
--- /dev/null
+++ b/app/src/main/res/layout/activity_bill_details.xml
@@ -0,0 +1,633 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_bill_history.xml b/app/src/main/res/layout/fragment_bill_history.xml
index a4e4da7..6e5bbfa 100644
--- a/app/src/main/res/layout/fragment_bill_history.xml
+++ b/app/src/main/res/layout/fragment_bill_history.xml
@@ -1,29 +1,145 @@
-
-
+
+ android:layout_margin="16dp"
+ android:padding="16dp"
+ android:text="Error loading data"
+ android:textColor="@android:color/holo_red_dark"
+ android:background="@drawable/card_background"
+ android:gravity="center"
+ android:visibility="gone" />
-
+
+
+
+ android:layout_height="wrap_content">
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_bill_card.xml b/app/src/main/res/layout/item_bill_card.xml
new file mode 100644
index 0000000..f605202
--- /dev/null
+++ b/app/src/main/res/layout/item_bill_card.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_subscription_card.xml b/app/src/main/res/layout/item_subscription_card.xml
new file mode 100644
index 0000000..1a1f00c
--- /dev/null
+++ b/app/src/main/res/layout/item_subscription_card.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..e3640e5
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file