render subscriptions

This commit is contained in:
2025-07-25 03:12:12 +05:00
parent 245ba50172
commit eecd4b0f1d
10 changed files with 515 additions and 37 deletions

View File

@@ -65,7 +65,8 @@ data class SubscriptionDetails(
val hasSmartMeter: Boolean, val hasSmartMeter: Boolean,
val category: ServiceCategory, val category: ServiceCategory,
val customer: Customer, val customer: Customer,
val lastBill: LastBill? val lastBill: LastBill?,
val subscriptionAddress: SubscriptionAddress?
) )
data class ServiceCategory( data class ServiceCategory(
@@ -85,7 +86,25 @@ data class Customer(
val id: Long, val id: Long,
val name: String, val name: String,
val accountNumber: String, val accountNumber: String,
val phone: String val phone: String,
val type: String
)
data class SubscriptionAddress(
val id: Long,
val type: String,
val property: Property?
)
data class Property(
val id: Long,
val name: String,
val street: Street?
)
data class Street(
val id: Long,
val name: String
) )
data class LastBill( data class LastBill(

View File

@@ -0,0 +1,77 @@
package sh.sar.gridflow.ui.subscriptions
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import sh.sar.gridflow.data.CustomerSubscription
import sh.sar.gridflow.databinding.ItemSubscriptionDetailedBinding
class DetailedSubscriptionsAdapter : ListAdapter<CustomerSubscription, DetailedSubscriptionsAdapter.SubscriptionViewHolder>(SubscriptionDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
val binding = ItemSubscriptionDetailedBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return SubscriptionViewHolder(binding)
}
override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
holder.bind(getItem(position))
}
class SubscriptionViewHolder(
private val binding: ItemSubscriptionDetailedBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(subscription: CustomerSubscription) {
with(binding) {
// Service icon and category
val serviceIcon = when (subscription.subscription.serviceId) {
1 -> "" // Electrical
2 -> "💧" // Water
else -> "🔌" // Unknown
}
textServiceIcon.text = serviceIcon
// Subscription info
textSubscriptionName.text = subscription.name
textSubscriptionNumber.text = "Sub #${subscription.subscription.subscriptionNumber}"
textServiceCategory.text = subscription.subscription.category.name.uppercase()
// Property information
val propertyName = subscription.subscription.subscriptionAddress?.property?.name ?: "Unknown Property"
val streetName = subscription.subscription.subscriptionAddress?.property?.street?.name ?: "Unknown Street"
textPropertyName.text = propertyName
textPropertyStreet.text = streetName
// Customer information
textCustomerName.text = subscription.subscription.customer.name
textCustomerPhone.text = subscription.subscription.customer.phone
// Action buttons - show coming soon toasts
btnEdit.setOnClickListener {
Toast.makeText(it.context, "Edit subscription coming soon", Toast.LENGTH_SHORT).show()
}
btnDelete.setOnClickListener {
Toast.makeText(it.context, "Delete subscription coming soon", Toast.LENGTH_SHORT).show()
}
}
}
}
private class SubscriptionDiffCallback : DiffUtil.ItemCallback<CustomerSubscription>() {
override fun areItemsTheSame(oldItem: CustomerSubscription, newItem: CustomerSubscription): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: CustomerSubscription, newItem: CustomerSubscription): Boolean {
return oldItem == newItem
}
}
}

View File

@@ -7,35 +7,109 @@ import android.view.ViewGroup
import android.widget.Toast 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.FragmentSubscriptionsBinding import sh.sar.gridflow.databinding.FragmentSubscriptionsBinding
class SubscriptionsFragment : Fragment() { class SubscriptionsFragment : Fragment() {
private var _binding: FragmentSubscriptionsBinding? = null private var _binding: FragmentSubscriptionsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var subscriptionsViewModel: SubscriptionsViewModel
private lateinit var subscriptionsAdapter: DetailedSubscriptionsAdapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
val subscriptionsViewModel = subscriptionsViewModel = ViewModelProvider(
ViewModelProvider(this).get(SubscriptionsViewModel::class.java) this,
ViewModelProvider.AndroidViewModelFactory.getInstance(requireActivity().application)
).get(SubscriptionsViewModel::class.java)
_binding = FragmentSubscriptionsBinding.inflate(inflater, container, false) _binding = FragmentSubscriptionsBinding.inflate(inflater, container, false)
val root: View = binding.root
setupViews()
val textView = binding.textSubscriptions setupObservers()
subscriptionsViewModel.text.observe(viewLifecycleOwner) {
textView.text = it return binding.root
}
private fun setupViews() {
// Setup RecyclerView
subscriptionsAdapter = DetailedSubscriptionsAdapter()
binding.recyclerSubscriptions.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = subscriptionsAdapter
} }
// Handle FAB click // Handle FAB click
binding.fabAddSubscription.setOnClickListener { binding.fabAddSubscription.setOnClickListener {
Toast.makeText(context, "Add subscription coming soon", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Add subscription coming soon", Toast.LENGTH_SHORT).show()
} }
return root // Handle retry button
binding.btnRetry.setOnClickListener {
subscriptionsViewModel.refreshSubscriptions()
}
}
private fun setupObservers() {
// Subscriptions data
subscriptionsViewModel.subscriptions.observe(viewLifecycleOwner) { subscriptions ->
subscriptionsAdapter.submitList(subscriptions)
// Show appropriate state
if (subscriptions.isEmpty()) {
showEmptyState()
} else {
showSubscriptionsList()
}
}
// Loading state
subscriptionsViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
if (isLoading) {
showLoadingState()
}
}
// Error state
subscriptionsViewModel.error.observe(viewLifecycleOwner) { error ->
if (error != null) {
showErrorState(error)
}
}
}
private fun showLoadingState() {
binding.progressLoading.visibility = View.VISIBLE
binding.recyclerSubscriptions.visibility = View.GONE
binding.layoutError.visibility = View.GONE
binding.layoutEmpty.visibility = View.GONE
}
private fun showSubscriptionsList() {
binding.progressLoading.visibility = View.GONE
binding.recyclerSubscriptions.visibility = View.VISIBLE
binding.layoutError.visibility = View.GONE
binding.layoutEmpty.visibility = View.GONE
}
private fun showEmptyState() {
binding.progressLoading.visibility = View.GONE
binding.recyclerSubscriptions.visibility = View.GONE
binding.layoutError.visibility = View.GONE
binding.layoutEmpty.visibility = View.VISIBLE
}
private fun showErrorState(error: String) {
binding.progressLoading.visibility = View.GONE
binding.recyclerSubscriptions.visibility = View.GONE
binding.layoutError.visibility = View.VISIBLE
binding.layoutEmpty.visibility = View.GONE
binding.textError.text = error
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -1,13 +1,69 @@
package sh.sar.gridflow.ui.subscriptions package sh.sar.gridflow.ui.subscriptions
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.CustomerSubscription
import sh.sar.gridflow.network.ApiResult
import sh.sar.gridflow.network.FenakaApiService
import sh.sar.gridflow.utils.SecureStorage
class SubscriptionsViewModel : ViewModel() { class SubscriptionsViewModel(application: Application) : AndroidViewModel(application) {
private val _text = MutableLiveData<String>().apply { private val secureStorage = SecureStorage(application)
value = "Subscriptions\n\nComing soon..." private val apiService = FenakaApiService()
companion object {
private const val TAG = "SubscriptionsViewModel"
}
private val _subscriptions = MutableLiveData<List<CustomerSubscription>>()
val subscriptions: LiveData<List<CustomerSubscription>> = _subscriptions
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
init {
loadSubscriptions()
}
fun loadSubscriptions() {
val cookie = secureStorage.getCookie()
if (cookie == null) {
Log.d(TAG, "No cookie found, cannot load subscriptions")
_error.value = "Authentication required"
return
}
_isLoading.value = true
_error.value = null
viewModelScope.launch {
Log.d(TAG, "Loading customer subscriptions")
when (val result = apiService.getCustomerSubscriptions("connect.sid=$cookie")) {
is ApiResult.Success -> {
Log.d(TAG, "Subscriptions loaded successfully: ${result.data.size} found")
_isLoading.value = false
_subscriptions.value = result.data
} is ApiResult.Error -> {
Log.d(TAG, "Failed to load subscriptions: ${result.message}")
_isLoading.value = false
_error.value = result.message
}
}
}
}
fun refreshSubscriptions() {
Log.d(TAG, "Refreshing subscriptions")
loadSubscriptions()
} }
val text: LiveData<String> = _text
} }

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/card_background_color" />
<corners android:radius="12dp" />
<stroke
android:width="1dp"
android:color="@color/card_stroke_color" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/category_tag_background" />
<corners android:radius="8dp" />
</shape>

View File

@@ -1,37 +1,86 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" 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:background="?android:attr/colorBackground" android:background="?android:attr/colorBackground"
tools:context=".ui.subscriptions.SubscriptionsFragment"> tools:context=".ui.subscriptions.SubscriptionsFragment">
<ScrollView <!-- Loading indicator -->
<ProgressBar
android:id="@+id/progress_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
<!-- Error message -->
<LinearLayout
android:id="@+id/layout_error"
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:gravity="center"
android:padding="32dp"
android:visibility="gone">
<LinearLayout <TextView
android:layout_width="match_parent" android:id="@+id/text_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:textSize="16sp"
android:padding="20dp" android:textColor="?android:attr/textColorPrimary"
android:gravity="center"> android:gravity="center"
android:layout_marginBottom="16dp"
tools:text="Failed to load subscriptions" />
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/text_subscriptions" android:id="@+id/btn_retry"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="18sp" android:text="Retry" />
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginTop="100dp"
tools:text="Subscriptions\n\nComing soon..." />
</LinearLayout> </LinearLayout>
</ScrollView> <!-- Empty state -->
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No subscriptions found"
android:textSize="18sp"
android:textColor="?android:attr/textColorPrimary"
android:gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add a new subscription to get started"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:gravity="center" />
</LinearLayout>
<!-- Subscriptions list -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_subscriptions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="80dp"
tools:listitem="@layout/item_subscription_detailed" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_subscription" android:id="@+id/fab_add_subscription"
@@ -42,6 +91,6 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:src="@drawable/ic_add_24" android:src="@drawable/ic_add_24"
android:contentDescription="Add subscription" android:contentDescription="Add subscription"
app:tint="?android:attr/colorBackground" /> android:tint="?android:attr/colorBackground" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="4dp"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/card_background"
android:elevation="2dp">
<!-- Header with service icon and name -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<TextView
android:id="@+id/text_service_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:gravity="center"
android:textSize="16sp"
android:background="@drawable/circle_background"
android:layout_marginEnd="12dp"
tools:text="⚡" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_subscription_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
tools:text="Nalahiya" />
<TextView
android:id="@+id/text_subscription_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
tools:text="Sub #98738" />
</LinearLayout>
<TextView
android:id="@+id/text_service_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:background="@drawable/category_tag_background"
android:textAllCaps="true"
tools:text="DOMESTIC" />
</LinearLayout>
<!-- Property and Customer Information Side by Side -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<!-- Property Information -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Property"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorSecondary"
android:textAllCaps="true"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/text_property_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="2dp"
tools:text="NALAHIYA" />
<TextView
android:id="@+id/text_property_street"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="?android:attr/textColorSecondary"
tools:text="EDHURU ALIBE HIN'GUN" />
</LinearLayout>
<!-- Customer Information -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Customer"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorSecondary"
android:textAllCaps="true"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/text_customer_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="13sp"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="2dp"
tools:text="Muaaviyath Mohamed" />
<TextView
android:id="@+id/text_customer_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textColor="?android:attr/textColorSecondary"
tools:text="9873221" />
</LinearLayout>
</LinearLayout>
<!-- Action Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_edit"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Edit"
android:textSize="11sp"
android:minHeight="36dp"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_delete"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Delete"
android:textSize="11sp"
android:minHeight="36dp"
android:textColor="?android:attr/colorError" />
</LinearLayout>
</LinearLayout>

View File

@@ -14,4 +14,7 @@
<color name="icon_circle_border">#555555</color> <color name="icon_circle_border">#555555</color>
<color name="selected_subscription_background">#1E3A8A</color> <color name="selected_subscription_background">#1E3A8A</color>
<color name="selected_subscription_border">#3B82F6</color> <color name="selected_subscription_border">#3B82F6</color>
<color name="category_tag_background">#424242</color>
<color name="card_stroke_color">#222222</color>
<color name="card_background_color">#121212</color>
</resources> </resources>

View File

@@ -14,4 +14,7 @@
<color name="icon_circle_border">#BBDEFB</color> <color name="icon_circle_border">#BBDEFB</color>
<color name="selected_subscription_background">#E3F2FD</color> <color name="selected_subscription_background">#E3F2FD</color>
<color name="selected_subscription_border">#90CAF9</color> <color name="selected_subscription_border">#90CAF9</color>
<color name="category_tag_background">#E0E0E0</color>
<color name="card_stroke_color">#E0E0E0</color>
<color name="card_background_color">#FFFFFF</color>
</resources> </resources>