From 43477f9d47c37015d60a1a8de39b9bb66dbec692 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 26 Jul 2025 21:07:45 +0500 Subject: [PATCH] add/delete and edit subs works now --- .../sar/gridflow/network/FenakaApiService.kt | 104 ++++++++++ .../ui/subscriptions/SubscriptionsFragment.kt | 183 ++++++++++++++++-- .../subscriptions/SubscriptionsViewModel.kt | 98 ++++++++++ .../res/layout/fragment_subscriptions.xml | 183 +++++++++++++++++- 4 files changed, 555 insertions(+), 13 deletions(-) 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 a86568e..80f706d 100644 --- a/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt +++ b/app/src/main/java/sh/sar/gridflow/network/FenakaApiService.kt @@ -640,6 +640,110 @@ class FenakaApiService { val matchResult = regex.find(html) return matchResult?.groupValues?.get(1) } + + suspend fun deleteCustomerSubscription(subscriptionId: Long, cookie: String): ApiResult = 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 = 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 { diff --git a/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsFragment.kt b/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsFragment.kt index 5b52f81..6b5a747 100644 --- a/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsFragment.kt +++ b/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsFragment.kt @@ -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() diff --git a/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsViewModel.kt b/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsViewModel.kt index 0aac8e7..c297d06 100644 --- a/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsViewModel.kt +++ b/app/src/main/java/sh/sar/gridflow/ui/subscriptions/SubscriptionsViewModel.kt @@ -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() val error: LiveData = _error + private val _billLookupResult = MutableLiveData() + val billLookupResult: LiveData = _billLookupResult + + private val _isLookingUp = MutableLiveData() + val isLookingUp: LiveData = _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) + } + } + } + } } diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index 2904e0f..028ff47 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -127,6 +127,7 @@ @@ -182,6 +183,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -