added bill history

This commit is contained in:
2025-07-26 01:09:48 +05:00
parent 86d9904b13
commit 8a60805bc5
19 changed files with 2240 additions and 36 deletions

View File

@@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
@@ -60,6 +62,21 @@
android:exported="false"
android:label="Reset Password OTP"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<activity
android:name=".ui.billhistory.BillDetailsActivity"
android:exported="false"
android:label="Bill Details"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -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<BillUnit>,
val billCharges: List<BillCharge>,
val billedReadings: List<BilledReading>,
val outstandingBillAmounts: List<Any>,
val billFineViews: List<Any>,
val billables: List<Billable>,
val outstandingBills: List<Any>,
val outstandingBillTotal: Double,
val meter: BilledReading,
val measurementUnit: String,
val billableFuelDiscount: Billable?,
val billableFuelSurcharge: Billable?,
val billableTariff: Billable?,
val billableItem: List<Billable>,
val billableMisc: List<Any>,
val billDiscount: List<Any>,
val otherBillables: List<Any>,
val billableFine: List<Any>,
val unitMeterUnits: List<Any>,
val normalMeterUnits: List<BillUnit>,
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<BillableItem>,
val billableMiscs: List<Any>
) : 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

View File

@@ -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<List<Bill>> = 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<Bill>::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<String> = 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<T> {

View File

@@ -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<BillCardAdapter.BillViewHolder>() {
private var bills: List<Bill> = emptyList()
fun updateBills(newBills: List<Bill>) {
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"
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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<String>().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<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _errorMessage = MutableLiveData<String?>()
val errorMessage: LiveData<String?> = _errorMessage
private val _subscriptions = MutableLiveData<List<CustomerSubscription>>()
val subscriptions: LiveData<List<CustomerSubscription>> = _subscriptions
private val _bills = MutableLiveData<List<Bill>>()
val bills: LiveData<List<Bill>> = _bills
private val _selectedSubscription = MutableLiveData<CustomerSubscription?>()
val selectedSubscription: LiveData<CustomerSubscription?> = _selectedSubscription
private val _selectedBill = MutableLiveData<Bill?>()
val selectedBill: LiveData<Bill?> = _selectedBill
private val _pdfBase64 = MutableLiveData<String?>()
val pdfBase64: LiveData<String?> = _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<Bill> {
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<String> = _text
}

View File

@@ -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<SubscriptionCardAdapter.SubscriptionViewHolder>() {
private var subscriptions: List<CustomerSubscription> = emptyList()
private var selectedSubscriptionId: Long? = null
fun updateSubscriptions(newSubscriptions: List<CustomerSubscription>) {
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)
}
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Safe system background color -->
<item android:color="@android:color/background_light"/>
<!-- Theme-aware background color for table headers -->
<item android:color="?android:attr/colorControlHighlight"/>
</selector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,21h-1l1,-7H7.5c-0.58,0 -0.57,-0.32 -0.38,-0.66 0.19,-0.34 0.05,-0.08 0.07,-0.12C8.48,10.94 10.42,7.54 13,3h1l-1,7h3.5c0.49,0 0.56,0.33 0.47,0.51l-0.07,0.15C12.96,17.55 11,21 11,21z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2c1.1,0 2,0.9 2,2c0,0.78 -0.99,2.44 -2,4.5c-1.01,-2.06 -2,-3.72 -2,-4.5C10,2.9 10.9,2 12,2zM21,9c0,-7.11 -9,-7.11 -9,0v1.09C7.93,8.84 4,12.31 4,17c0,4.42 4.03,8 9,8s9,-3.58 9,-8C22,12.31 18.07,8.84 14,10.09V9H21z"/>
</vector>

View File

@@ -0,0 +1,633 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/colorBackground">
<!-- Toolbar -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:elevation="4dp"
app:title="Bill Details"
app:titleTextColor="@android:color/white"
app:navigationIcon="@drawable/ic_arrow_back"
app:navigationIconTint="@android:color/white" />
<!-- Loading indicator -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:visibility="gone" />
<!-- Error message -->
<TextView
android:id="@+id/errorMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:padding="16dp"
android:text="Error loading bill details"
android:textColor="@android:color/holo_red_dark"
android:background="@drawable/card_background"
android:gravity="center"
android:visibility="gone" />
<!-- Main content -->
<ScrollView
android:id="@+id/mainContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Card 1: Bill Information -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/billInfoCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bill Information"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/shareButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:insetTop="0dp"
android:insetBottom="0dp"
app:icon="@drawable/ic_share_24"
app:iconTint="@android:color/white"
app:iconPadding="0dp"
style="@style/Widget.Material3.Button.IconButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/downloadButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:insetTop="0dp"
android:insetBottom="0dp"
app:icon="@drawable/ic_download_24"
app:iconTint="@android:color/white"
app:iconPadding="0dp"
style="@style/Widget.Material3.Button.IconButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/openButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:insetTop="0dp"
android:insetBottom="0dp"
app:icon="@drawable/ic_open_24"
app:iconTint="@android:color/white"
app:iconPadding="0dp"
android:visibility="visible"
style="@style/Widget.Material3.Button.IconButton" />
<ProgressBar
android:id="@+id/pdfLoadingSpinner"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:indeterminateTint="@android:color/white" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bill Number:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/billNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="17-120072"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bill Total:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/billTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MVR 320.83"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Paid Date:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/paidDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="July 1, 2025"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/holo_green_dark" />
<com.google.android.material.button.MaterialButton
android:id="@+id/payNowButton"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="Pay Now"
android:textSize="12sp"
android:backgroundTint="@android:color/holo_red_light"
android:textColor="@android:color/white"
android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Bill Date:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/billDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="June 29, 2025"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Card 2: Subscription Details -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<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="Subscription Details"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Subscription No:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/subscriptionNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="8584"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Account No:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/accountNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="16656"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Branch:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/branch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HDH.Kulhudhufushi"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Meter No:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/meterNo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="6211897"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Tariff Code:"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tariffCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="D"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Card 3: Meter Readings -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<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="Meter Readings"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp" />
<!-- Table header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/account_dropdown_background"
android:padding="8dp"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Month"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Reading"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Usage"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="end" />
</LinearLayout>
<!-- Current Month Table row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Current"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/readingValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="37992"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center" />
<TextView
android:id="@+id/usageValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="323 kwh"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="end" />
</LinearLayout>
<!-- Previous Month Table row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/account_dropdown_background">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Previous"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/previousReadingValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="37669"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center" />
<TextView
android:id="@+id/previousUsageValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="123 kWh"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="end" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Card 4: Monthly Statement -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp">
<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="Monthly Statement"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="16dp" />
<!-- Table header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/account_dropdown_background"
android:padding="8dp"
android:layout_marginBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Qty"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Rate"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Amount"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:gravity="end" />
</LinearLayout>
<!-- Dynamic charges will be added here -->
<LinearLayout
android:id="@+id/chargesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -1,29 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:orientation="vertical"
android:background="?android:attr/colorBackground"
tools:context=".ui.billhistory.BillHistoryFragment">
<LinearLayout
<!-- Error message -->
<TextView
android:id="@+id/errorMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp"
android:gravity="center">
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" />
<TextView
android:id="@+id/text_bill_history"
<!-- Fixed Subscriptions Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_subscriptions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginTop="100dp"
tools:text="Bill History\n\nComing soon..." />
android:layout_height="wrap_content">
</LinearLayout>
<LinearLayout
android:id="@+id/subscriptions_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
</ScrollView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Subscriptions"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="12dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/subscriptionsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="8dp" />
</LinearLayout>
<!-- Loading indicator inside subscription card -->
<LinearLayout
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
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 subscriptions..."
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Fixed Bills Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_bills"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/billsTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bills"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="12dp" />
<!-- Scrollable bills content -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/billsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="8dp" />
<TextView
android:id="@+id/noBillsMessage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="No bills found for this subscription"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
android:gravity="center"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/billDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="June 2025"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/billAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MVR 320.83"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<TextView
android:id="@+id/billNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Bill #17-120072"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/paymentStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Paid on July 1, 2025"
android:textSize="12sp"
android:textColor="@android:color/holo_green_dark" />
<com.google.android.material.button.MaterialButton
android:id="@+id/payNowButton"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:text="Pay Now"
android:textSize="12sp"
android:backgroundTint="@android:color/holo_red_light"
android:textColor="@android:color/white"
android:visibility="gone"
style="@style/Widget.Material3.Button.TextButton" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/card_background"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/serviceIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_electric_24"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="8dp"
app:tint="?android:attr/textColorPrimary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/subscriptionName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subscription Name"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/subscriptionDetails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Subscription Details"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="." />
<external-path name="external" path="." />
<external-files-path name="external_files" path="." />
<external-cache-path name="external_cache" path="." />
</paths>