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