added bill history
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -60,6 +62,21 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="Reset Password OTP"
|
android:label="Reset Password OTP"
|
||||||
android:theme="@style/Theme.GridFlow.NoActionBar" />
|
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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@@ -1,5 +1,7 @@
|
|||||||
package sh.sar.gridflow.data
|
package sh.sar.gridflow.data
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
data class LoginRequest(
|
data class LoginRequest(
|
||||||
val mobile: String,
|
val mobile: String,
|
||||||
val password: String
|
val password: String
|
||||||
@@ -80,7 +82,7 @@ data class MainCategory(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val code: String
|
val code: String
|
||||||
)
|
) : Serializable
|
||||||
|
|
||||||
data class Customer(
|
data class Customer(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@@ -88,24 +90,24 @@ data class Customer(
|
|||||||
val accountNumber: String,
|
val accountNumber: String,
|
||||||
val phone: String,
|
val phone: String,
|
||||||
val type: String
|
val type: String
|
||||||
)
|
) : Serializable
|
||||||
|
|
||||||
data class SubscriptionAddress(
|
data class SubscriptionAddress(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val type: String,
|
val type: String,
|
||||||
val property: Property?
|
val property: Property?
|
||||||
)
|
) : Serializable
|
||||||
|
|
||||||
data class Property(
|
data class Property(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
val street: Street?
|
val street: Street?
|
||||||
)
|
) : Serializable
|
||||||
|
|
||||||
data class Street(
|
data class Street(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
) : Serializable
|
||||||
|
|
||||||
data class LastBill(
|
data class LastBill(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@@ -158,3 +160,256 @@ data class BandRate(
|
|||||||
val band: String,
|
val band: String,
|
||||||
val rate: 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
|
||||||
|
@@ -9,6 +9,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import sh.sar.gridflow.data.BandRatesResponse
|
import sh.sar.gridflow.data.BandRatesResponse
|
||||||
|
import sh.sar.gridflow.data.Bill
|
||||||
import sh.sar.gridflow.data.CustomerSubscription
|
import sh.sar.gridflow.data.CustomerSubscription
|
||||||
import sh.sar.gridflow.data.ErrorResponse
|
import sh.sar.gridflow.data.ErrorResponse
|
||||||
import sh.sar.gridflow.data.ForgotPasswordRequest
|
import sh.sar.gridflow.data.ForgotPasswordRequest
|
||||||
@@ -371,6 +372,87 @@ class FenakaApiService {
|
|||||||
ApiResult.Error("Unexpected error: ${e.message}", -1)
|
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> {
|
sealed class ApiResult<T> {
|
||||||
|
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,37 +1,147 @@
|
|||||||
package sh.sar.gridflow.ui.billhistory
|
package sh.sar.gridflow.ui.billhistory
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import sh.sar.gridflow.databinding.FragmentBillHistoryBinding
|
import sh.sar.gridflow.databinding.FragmentBillHistoryBinding
|
||||||
|
import sh.sar.gridflow.data.Bill
|
||||||
|
|
||||||
class BillHistoryFragment : Fragment() {
|
class BillHistoryFragment : Fragment() {
|
||||||
|
|
||||||
private var _binding: FragmentBillHistoryBinding? = null
|
private var _binding: FragmentBillHistoryBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private lateinit var billHistoryViewModel: BillHistoryViewModel
|
||||||
|
private lateinit var subscriptionAdapter: SubscriptionCardAdapter
|
||||||
|
private lateinit var billAdapter: BillCardAdapter
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
val billHistoryViewModel =
|
billHistoryViewModel = ViewModelProvider(this)[BillHistoryViewModel::class.java]
|
||||||
ViewModelProvider(this).get(BillHistoryViewModel::class.java)
|
|
||||||
|
|
||||||
_binding = FragmentBillHistoryBinding.inflate(inflater, container, false)
|
_binding = FragmentBillHistoryBinding.inflate(inflater, container, false)
|
||||||
val root: View = binding.root
|
val root: View = binding.root
|
||||||
|
|
||||||
val textView = binding.textBillHistory
|
setupRecyclerViews()
|
||||||
billHistoryViewModel.text.observe(viewLifecycleOwner) {
|
setupObservers()
|
||||||
textView.text = it
|
setupClickListeners()
|
||||||
}
|
|
||||||
|
|
||||||
return root
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
|
@@ -1,13 +1,198 @@
|
|||||||
package sh.sar.gridflow.ui.billhistory
|
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.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
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() {
|
class BillHistoryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val _text = MutableLiveData<String>().apply {
|
companion object {
|
||||||
value = "Bill History\n\nComing soon..."
|
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
|
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- Safe system background color -->
|
<!-- Theme-aware background color for table headers -->
|
||||||
<item android:color="@android:color/background_light"/>
|
<item android:color="?android:attr/colorControlHighlight"/>
|
||||||
</selector>
|
</selector>
|
||||||
|
5
app/src/main/res/drawable/ic_download_24.xml
Normal file
5
app/src/main/res/drawable/ic_download_24.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_electric_24.xml
Normal file
5
app/src/main/res/drawable/ic_electric_24.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_open_24.xml
Normal file
5
app/src/main/res/drawable/ic_open_24.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_share_24.xml
Normal file
5
app/src/main/res/drawable/ic_share_24.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_water_24.xml
Normal file
5
app/src/main/res/drawable/ic_water_24.xml
Normal 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>
|
633
app/src/main/res/layout/activity_bill_details.xml
Normal file
633
app/src/main/res/layout/activity_bill_details.xml
Normal 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>
|
@@ -1,29 +1,145 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fillViewport="true"
|
android:orientation="vertical"
|
||||||
android:background="?android:attr/colorBackground"
|
android:background="?android:attr/colorBackground"
|
||||||
tools:context=".ui.billhistory.BillHistoryFragment">
|
tools:context=".ui.billhistory.BillHistoryFragment">
|
||||||
|
|
||||||
<LinearLayout
|
<!-- Error message -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/errorMessage"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:layout_margin="16dp"
|
||||||
android:padding="20dp"
|
android:padding="16dp"
|
||||||
android:gravity="center">
|
android:text="Error loading data"
|
||||||
|
android:textColor="@android:color/holo_red_dark"
|
||||||
|
android:background="@drawable/card_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<!-- Fixed Subscriptions Card -->
|
||||||
android:id="@+id/text_bill_history"
|
<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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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..." />
|
|
||||||
|
|
||||||
</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>
|
||||||
|
87
app/src/main/res/layout/item_bill_card.xml
Normal file
87
app/src/main/res/layout/item_bill_card.xml
Normal 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>
|
60
app/src/main/res/layout/item_subscription_card.xml
Normal file
60
app/src/main/res/layout/item_subscription_card.xml
Normal 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>
|
7
app/src/main/res/xml/file_paths.xml
Normal file
7
app/src/main/res/xml/file_paths.xml
Normal 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>
|
Reference in New Issue
Block a user