add/delete and edit subs works now

This commit is contained in:
2025-07-26 21:07:45 +05:00
parent 26c3b7418c
commit 43477f9d47
4 changed files with 555 additions and 13 deletions

View File

@@ -640,6 +640,110 @@ class FenakaApiService {
val matchResult = regex.find(html)
return matchResult?.groupValues?.get(1)
}
suspend fun deleteCustomerSubscription(subscriptionId: Long, cookie: String): ApiResult<Unit> = withContext(Dispatchers.IO) {
Log.d(TAG, "Deleting customer subscription: $subscriptionId")
val cookieHeader = "connect.sid=$cookie"
val request = Request.Builder()
.url("$BASE_URL/api/customer-subscriptions/$subscriptionId")
.delete()
.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, "Delete subscription response code: ${response.code}")
when (response.code) {
200, 204 -> {
Log.d(TAG, "Subscription deleted successfully")
ApiResult.Success(Unit, null)
}
404 -> {
Log.d(TAG, "Subscription not found: ${response.code}")
ApiResult.Error("Subscription not found", response.code)
}
403 -> {
Log.d(TAG, "Access denied: ${response.code}")
ApiResult.Error("Access denied", response.code)
}
else -> {
Log.d(TAG, "Failed to delete subscription: ${response.code}")
ApiResult.Error("Failed to delete subscription", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during subscription deletion", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during subscription deletion", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
suspend fun addCustomerSubscription(name: String, subscriptionNumber: String, billNumber: String, cookie: String): ApiResult<Unit> = withContext(Dispatchers.IO) {
Log.d(TAG, "Adding customer subscription: $subscriptionNumber with name: $name")
val cookieHeader = "connect.sid=$cookie"
// Create request body
val requestBody = mapOf(
"name" to name,
"subscriptionNumber" to subscriptionNumber,
"billNumber" to billNumber
)
val jsonBody = gson.toJson(requestBody).toRequestBody(JSON_MEDIA_TYPE.toMediaType())
val request = Request.Builder()
.url("$BASE_URL/api/customer-subscriptions")
.post(jsonBody)
.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, "Add subscription response code: ${response.code}")
when (response.code) {
200, 201 -> {
Log.d(TAG, "Subscription added successfully")
ApiResult.Success(Unit, null)
}
400 -> {
Log.d(TAG, "Bad request: ${response.code}")
ApiResult.Error("Invalid subscription data", response.code)
}
409 -> {
Log.d(TAG, "Subscription already exists: ${response.code}")
ApiResult.Error("Subscription already exists", response.code)
}
403 -> {
Log.d(TAG, "Access denied: ${response.code}")
ApiResult.Error("Access denied", response.code)
}
else -> {
Log.d(TAG, "Failed to add subscription: ${response.code}")
ApiResult.Error("Failed to add subscription", response.code)
}
}
} catch (e: IOException) {
Log.e(TAG, "Network error during subscription addition", e)
ApiResult.Error("Network error: ${e.message}", -1)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error during subscription addition", e)
ApiResult.Error("Unexpected error: ${e.message}", -1)
}
}
}
sealed class ApiResult<T> {

View File

@@ -1,14 +1,19 @@
package sh.sar.gridflow.ui.subscriptions
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import sh.sar.gridflow.data.BillLookupResponse
import sh.sar.gridflow.data.CustomerSubscription
import sh.sar.gridflow.databinding.DialogDeleteSubscriptionBinding
import sh.sar.gridflow.databinding.DialogEditSubscriptionBinding
@@ -63,13 +68,40 @@ class SubscriptionsFragment : Fragment() {
// Handle continue button click
binding.btnContinue.setOnClickListener {
val subscriptionNumber = binding.editSubscriptionNumber.text.toString().trim()
val billNumber = binding.editBillNumber.text.toString().trim()
val alias = binding.editAlias.text.toString().trim()
if (binding.btnContinue.text == "Add Subscription") {
// Card is showing, add the subscription
val alias = binding.editAlias.text.toString().trim()
val subscriptionNumber = binding.editSubscriptionNumber.text.toString().trim()
val billNumber = binding.editBillNumber.text.toString().trim()
if (alias.isNotEmpty()) {
subscriptionsViewModel.addSubscription(
name = alias,
subscriptionNumber = subscriptionNumber,
billNumber = billNumber,
onSuccess = {
Toast.makeText(requireContext(), "Subscription added successfully", Toast.LENGTH_SHORT).show()
hideAddSubscriptionForm()
},
onError = { errorMessage ->
Toast.makeText(requireContext(), "Failed to add subscription: $errorMessage", Toast.LENGTH_SHORT).show()
}
)
} else {
binding.editAlias.error = "Alias/Description is required"
}
} else {
// Input fields are showing, do lookup
val subscriptionNumber = binding.editSubscriptionNumber.text.toString().trim()
val billNumber = binding.editBillNumber.text.toString().trim()
val alias = binding.editAlias.text.toString().trim()
if (validateInputs(subscriptionNumber, billNumber, alias)) {
Toast.makeText(requireContext(), "Continue action not implemented yet", Toast.LENGTH_SHORT).show()
hideAddSubscriptionForm()
if (validateInputs(subscriptionNumber, billNumber, alias)) {
// First, lookup the bill to get customer info
subscriptionsViewModel.lookupBill(billNumber, subscriptionNumber) { errorMessage ->
Toast.makeText(requireContext(), "Failed to find subscription: $errorMessage", Toast.LENGTH_SHORT).show()
}
}
}
}
@@ -105,6 +137,24 @@ class SubscriptionsFragment : Fragment() {
showErrorState(error)
}
}
// Bill lookup result
subscriptionsViewModel.billLookupResult.observe(viewLifecycleOwner) { billLookup ->
if (billLookup != null) {
showCustomerInfo(billLookup)
} else {
hideCustomerInfo()
}
}
// Lookup loading state
subscriptionsViewModel.isLookingUp.observe(viewLifecycleOwner) { isLookingUp ->
if (isLookingUp) {
showLookupLoading()
} else {
hideLookupLoading()
}
}
}
private fun showLoadingState() {
@@ -145,14 +195,48 @@ class SubscriptionsFragment : Fragment() {
.create()
dialogBinding.btnCancel.setOnClickListener {
hideKeyboard()
dialog.dismiss()
}
dialogBinding.btnSubmit.setOnClickListener {
val newName = dialogBinding.editSubscriptionName.text.toString().trim()
if (newName.isNotEmpty()) {
Toast.makeText(context, "Edit API call needed for: $newName", Toast.LENGTH_SHORT).show()
dialog.dismiss()
hideKeyboard()
// Get bill number from subscription data - try lastBill first, then fallback
val billNumber = subscription.subscription.lastBill?.billNumber ?: ""
val subscriptionNumber = subscription.subscription.subscriptionNumber
if (billNumber.isEmpty()) {
Toast.makeText(context, "Cannot edit: Bill number not available", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// Edit = Delete + Add (since there's no edit API)
subscriptionsViewModel.deleteSubscription(
subscriptionId = subscription.id,
onSuccess = {
// After successful delete, add the subscription with new name
subscriptionsViewModel.addSubscription(
name = newName,
subscriptionNumber = subscriptionNumber,
billNumber = billNumber,
onSuccess = {
Toast.makeText(requireContext(), "Subscription updated successfully", Toast.LENGTH_SHORT).show()
dialog.dismiss()
},
onError = { errorMessage ->
Toast.makeText(requireContext(), "Failed to update subscription: $errorMessage", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
)
},
onError = { errorMessage ->
Toast.makeText(requireContext(), "Failed to update subscription: $errorMessage", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
)
} else {
dialogBinding.editSubscriptionName.error = "Name cannot be empty"
}
@@ -173,8 +257,17 @@ class SubscriptionsFragment : Fragment() {
}
dialogBinding.btnYes.setOnClickListener {
Toast.makeText(context, "Delete API call needed for: ${subscription.name}", Toast.LENGTH_SHORT).show()
dialog.dismiss()
subscriptionsViewModel.deleteSubscription(
subscriptionId = subscription.id,
onSuccess = {
Toast.makeText(context, "Subscription deleted successfully", Toast.LENGTH_SHORT).show()
dialog.dismiss()
},
onError = { errorMessage ->
Toast.makeText(context, "Failed to delete subscription: $errorMessage", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
)
}
dialog.show()
@@ -188,13 +281,22 @@ class SubscriptionsFragment : Fragment() {
binding.progressLoading.visibility = View.GONE
binding.fabAddSubscription.visibility = View.GONE
// Clear form fields
// Clear form fields and customer info
binding.editSubscriptionNumber.text?.clear()
binding.editBillNumber.text?.clear()
binding.editAlias.text?.clear()
subscriptionsViewModel.clearBillLookup()
// Ensure initial state is correct
binding.inputFieldsLayout.visibility = View.VISIBLE
binding.cardCustomerInfo.visibility = View.GONE
binding.btnContinue.text = "Continue"
binding.btnContinue.visibility = View.VISIBLE
binding.btnCancelAdd.visibility = View.VISIBLE
}
private fun hideAddSubscriptionForm() {
hideKeyboard()
binding.scrollAddForm.visibility = View.GONE
binding.fabAddSubscription.visibility = View.VISIBLE
@@ -228,6 +330,65 @@ class SubscriptionsFragment : Fragment() {
return isValid
}
private fun showCustomerInfo(billLookup: BillLookupResponse) {
// Hide keyboard when showing customer info
hideKeyboard()
// Hide input fields and show card
binding.inputFieldsLayout.visibility = View.GONE
binding.cardCustomerInfo.visibility = View.VISIBLE
// Change button text to "Add Subscription"
binding.btnContinue.text = "Add Subscription"
// Service type
val serviceType = when (billLookup.subscription.serviceId) {
1 -> "Electricity"
2 -> "Water"
else -> "Unknown Service"
}
// Customer information
binding.tvCustomerName.text = billLookup.customer.name
binding.tvPhoneNumber.text = billLookup.customer.phone
// Address information
val propertyName = billLookup.subscriptionAddress?.property?.name ?: "Unknown Property"
val streetName = billLookup.subscriptionAddress?.property?.street?.name ?: "Unknown Street"
binding.tvAddress.text = "$propertyName, $streetName"
// Service details
binding.tvSubscriptionNumber.text = billLookup.subscription.subscriptionNumber
binding.tvServiceType.text = serviceType
}
private fun hideCustomerInfo() {
binding.cardCustomerInfo.visibility = View.GONE
binding.inputFieldsLayout.visibility = View.VISIBLE
binding.btnContinue.text = "Continue"
// Ensure buttons are visible
binding.btnContinue.visibility = View.VISIBLE
binding.btnCancelAdd.visibility = View.VISIBLE
}
private fun showLookupLoading() {
binding.layoutLookupLoading.visibility = View.VISIBLE
binding.cardCustomerInfo.visibility = View.GONE
}
private fun hideLookupLoading() {
binding.layoutLookupLoading.visibility = View.GONE
}
private fun hideKeyboard() {
val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusView = activity?.currentFocus
if (currentFocusView != null) {
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
}
}
override fun onDestroyView() {
super.onDestroyView()

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import sh.sar.gridflow.data.BillLookupResponse
import sh.sar.gridflow.data.CustomerSubscription
import sh.sar.gridflow.network.ApiResult
import sh.sar.gridflow.network.FenakaApiService
@@ -30,6 +31,12 @@ class SubscriptionsViewModel(application: Application) : AndroidViewModel(applic
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
private val _billLookupResult = MutableLiveData<BillLookupResponse?>()
val billLookupResult: LiveData<BillLookupResponse?> = _billLookupResult
private val _isLookingUp = MutableLiveData<Boolean>()
val isLookingUp: LiveData<Boolean> = _isLookingUp
init {
loadSubscriptions()
}
@@ -66,4 +73,95 @@ class SubscriptionsViewModel(application: Application) : AndroidViewModel(applic
Log.d(TAG, "Refreshing subscriptions")
loadSubscriptions()
}
fun deleteSubscription(subscriptionId: Long, onSuccess: () -> Unit, onError: (String) -> Unit) {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Log.d(TAG, "No cookie found, cannot delete subscription")
onError("Authentication required")
return
}
viewModelScope.launch {
Log.d(TAG, "Deleting subscription: $subscriptionId")
when (val result = apiService.deleteCustomerSubscription(subscriptionId, cookie)) {
is ApiResult.Success -> {
Log.d(TAG, "Subscription deleted successfully")
loadSubscriptions() // Refresh the list
onSuccess()
}
is ApiResult.Error -> {
Log.d(TAG, "Failed to delete subscription: ${result.message}")
onError(result.message)
}
}
}
}
fun lookupBill(billNumber: String, subscriptionNumber: String, onError: (String) -> Unit) {
_isLookingUp.value = true
_billLookupResult.value = null
viewModelScope.launch {
Log.d(TAG, "Looking up bill: $billNumber, subscription: $subscriptionNumber")
when (val result = apiService.findBill(billNumber, subscriptionNumber)) {
is ApiResult.Success -> {
Log.d(TAG, "Bill lookup successful: ${result.data.customer.name}")
_isLookingUp.value = false
_billLookupResult.value = result.data
Log.d(TAG, "BillLookupResult LiveData updated")
}
is ApiResult.Error -> {
if (result.code == 404) {
Log.d(TAG, "404 received, trying with swapped parameters")
// Try again with swapped parameters
when (val retryResult = apiService.findBill(subscriptionNumber, billNumber)) {
is ApiResult.Success -> {
Log.d(TAG, "Retry successful with swapped parameters")
_isLookingUp.value = false
_billLookupResult.value = retryResult.data
}
is ApiResult.Error -> {
Log.d(TAG, "Retry also failed: ${retryResult.message}")
_isLookingUp.value = false
onError(retryResult.message)
}
}
} else {
Log.d(TAG, "Bill lookup failed: ${result.message}")
_isLookingUp.value = false
onError(result.message)
}
}
}
}
}
fun clearBillLookup() {
_billLookupResult.value = null
}
fun addSubscription(name: String, subscriptionNumber: String, billNumber: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Log.d(TAG, "No cookie found, cannot add subscription")
onError("Authentication required")
return
}
viewModelScope.launch {
Log.d(TAG, "Adding subscription: $subscriptionNumber with name: $name")
when (val result = apiService.addCustomerSubscription(name, subscriptionNumber, billNumber, cookie)) {
is ApiResult.Success -> {
Log.d(TAG, "Subscription added successfully")
loadSubscriptions() // Refresh the list
onSuccess()
}
is ApiResult.Error -> {
Log.d(TAG, "Failed to add subscription: ${result.message}")
onError(result.message)
}
}
}
}
}

View File

@@ -127,6 +127,7 @@
<!-- Form Fields -->
<LinearLayout
android:id="@+id/input_fields_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -182,6 +183,186 @@
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Subscription Details Card -->
<LinearLayout
android:id="@+id/card_customer_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:visibility="gone"
android:orientation="vertical"
android:background="?android:attr/colorBackground"
android:elevation="4dp"
android:padding="20dp">
<!-- Customer Details -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer Details"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Name:"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tv_customer_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Phone:"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tv_phone_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Address:"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<!-- Service Details -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service Details"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="4dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Subscription:"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tv_subscription_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Service:"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/tv_service_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2"
android:textSize="12sp"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>
</LinearLayout>
<!-- Loading indicator for lookup -->
<LinearLayout
android:id="@+id/layout_lookup_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginBottom="24dp"
android:visibility="gone">
<ProgressBar
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Looking up subscription details..."
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<!-- Buttons -->
<LinearLayout
android:layout_width="match_parent"
@@ -210,8 +391,6 @@
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>