- Approver for documents as Admin (#19)

- Approver for documents as Admin
 - UI tests for document approving
 - update config.yml
 - update android test suite
 - idling resources added for toast
 - toast methods refactored
 - tests for approving updated
This commit is contained in:
2023-06-20 09:12:47 +01:00
committed by GitHub
parent 786761f67c
commit 600f82d2a1
45 changed files with 715 additions and 189 deletions

View File

@@ -1,5 +1,6 @@
package h_mal.appttude.com.driver.application
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import h_mal.appttude.com.driver.data.FirebaseAuthSource
@@ -12,7 +13,8 @@ class ApplicationViewModelFactory(
private val auth: FirebaseAuthSource,
private val database: FirebaseDatabaseSource,
private val storage: FirebaseStorageSource,
private val preferences: PreferenceProvider
private val preferences: PreferenceProvider,
private val resources: Resources
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -57,6 +59,7 @@ class ApplicationViewModelFactory(
database,
preferences
)
isAssignableFrom(ApproverViewModel::class.java) -> ApproverViewModel(resources , database)
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}

View File

@@ -0,0 +1,31 @@
package h_mal.appttude.com.driver.application
import android.app.Application
import android.content.res.Resources
import h_mal.appttude.com.driver.data.FirebaseAuthSource
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.FirebaseStorageSource
import h_mal.appttude.com.driver.data.prefs.PreferenceProvider
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class DriverApplication : BaseApplication() {
override val flavourModule = super.flavourModule.copy {
bind() from singleton { PreferenceProvider(this@DriverApplication) }
bind() from provider {
ApplicationViewModelFactory(
instance(),
instance(),
instance(),
instance(),
instance()
)
}
}
}

View File

@@ -0,0 +1,19 @@
package h_mal.appttude.com.driver.model
import h_mal.appttude.com.driver.R
enum class ApprovalStatus(val stringId: Int, val drawableId: Int, val score: Int) {
NOT_SUBMITTED(R.string.not_submitted, R.drawable.denied, 0),
DENIED(R.string.denied, R.drawable.denied, 1),
PENDING_APPROVAL(R.string.pending, R.drawable.pending, 2),
APPROVED(R.string.approved, R.drawable.approved, 3);
companion object {
infix fun getByScore(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.score == value }
infix fun getByStringId(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.stringId == value }
infix fun getByDrawableId(value: Int): ApprovalStatus? =
ApprovalStatus.values().firstOrNull { it.drawableId == value }
}
}

View File

@@ -1,77 +1,57 @@
package h_mal.appttude.com.driver.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import h_mal.appttude.com.driver.R
import android.widget.ArrayAdapter
import h_mal.appttude.com.driver.databinding.ApprovalListItemBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.utils.hide
import java.io.IOException
class ApprovalListAdapter(
private val layoutInflater: LayoutInflater,
private var approvals: Map<String, Int?>,
private val context: Context,
private val data: List<Pair<String, ApprovalStatus>>,
private val callback: (String) -> Unit
) : BaseAdapter() {
override fun getCount(): Int = approvals.size
override fun getItem(position: Int): Map.Entry<String, Int?> = approvals.entries.elementAt(position)
override fun getItemId(position: Int): Long = position.toLong()
) : ArrayAdapter<Pair<String, ApprovalStatus>>(context, 0, data) {
override fun getCount(): Int = data.size
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var listItemView: View? = convertView
val binding: ApprovalListItemBinding
if (listItemView == null) {
binding = ApprovalListItemBinding.inflate(layoutInflater, parent, false)
// Inflate view binding into listview cell
binding = ApprovalListItemBinding.inflate(LayoutInflater.from(context), parent, false)
listItemView = binding.root
listItemView.setTag(listItemView.id, binding)
} else {
// cell exists so recycling view
binding = listItemView.getTag(listItemView.id) as ApprovalListItemBinding
}
val key = getItem(position).key
val itemValue = getItem(position).value
val key: String = getItem(position)?.first ?: throw IOException("No document name provided")
val approvalStatus: ApprovalStatus? = getItem(position)?.second
binding.approvalText.text = key
if (itemValue != 0) {
binding.root.setOnClickListener { callback.invoke(key) }
}
binding.approvalIv.setImageResource(getImageResourceBasedOnApproval(itemValue))
binding.approvalStatus.text = listItemView.context.getString(getStringResourceBasedOnApproval(itemValue))
if (position == 0) {
binding.divider.hide()
approvalStatus?.let { item ->
item.score.takeIf { it != 0 }?.let {
binding.root.setOnClickListener { callback.invoke(key) }
}
binding.approvalIv.setImageResource(item.drawableId)
binding.approvalStatus.text = context.getString(item.stringId)
}
// hide divider for first cell
if (position == 0) binding.divider.hide()
return (listItemView)
}
@DrawableRes
private fun getImageResourceBasedOnApproval(value: Int?): Int {
return when(value) {
0 -> R.drawable.denied
1 -> R.drawable.denied
2 -> R.drawable.pending
3 -> R.drawable.approved
else -> R.drawable.pending
}
}
@StringRes
private fun getStringResourceBasedOnApproval(value: Int?): Int {
return when(value) {
0 -> R.string.not_submitted
1 -> R.string.denied
2 -> R.string.pending
3 -> R.string.approved
else -> R.string.pending
}
}
fun updateAdapter(data: Map<String, Int?>) {
approvals = data
notifyDataSetChanged()
fun updateAdapter(date: List<Pair<String, ApprovalStatus>>) {
clear()
addAll(date)
}
}

View File

@@ -0,0 +1,40 @@
package h_mal.appttude.com.driver.ui
import com.google.android.material.snackbar.Snackbar
import h_mal.appttude.com.driver.base.BaseFragment
import h_mal.appttude.com.driver.databinding.FragmentApproverBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.viewmodels.ApproverViewModel
class ApproverFragment : BaseFragment<ApproverViewModel, FragmentApproverBinding>() {
override fun setupView(binding: FragmentApproverBinding) = binding.run {
super.setupView(binding)
val args = requireArguments()
viewModel.init(args)
// Retrieve fragment name argument saved from previous fragment
val fragmentClass = viewModel.getFragmentClass()
childFragmentManager.beginTransaction()
.replace(container.id, fragmentClass, args, null)
.commitNow()
approve.setOnClickListener { viewModel.approveDocument() }
decline.setOnClickListener { viewModel.declineDocument() }
}
override fun onSuccess(data: Any?) {
super.onSuccess(data)
when (data) {
ApprovalStatus.APPROVED -> displaySnackBar("approved")
ApprovalStatus.DENIED -> displaySnackBar("declined")
}
}
private fun displaySnackBar(status: String) {
showSnackBar("Document has been $status")
}
}

View File

@@ -1,11 +1,12 @@
package h_mal.appttude.com.driver.ui
import android.view.View
import android.widget.ListView
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.BaseFragment
import h_mal.appttude.com.driver.data.USER_CONST
import h_mal.appttude.com.driver.databinding.FragmentUserMainBinding
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.utils.FRAGMENT
import h_mal.appttude.com.driver.utils.navigateTo
import h_mal.appttude.com.driver.utils.toBundle
import h_mal.appttude.com.driver.viewmodels.DriverOverviewViewModel
@@ -19,40 +20,30 @@ class DriverOverviewFragment : BaseFragment<DriverOverviewViewModel, FragmentUse
override fun setupView(binding: FragmentUserMainBinding) {
listView = binding.approvalsList
}
driverId = requireArguments().getString(USER_CONST) ?: throw IOException("No user ID has been passed")
override fun onResume() {
super.onResume()
driverId = requireArguments().getString(USER_CONST)
?: throw IOException("No user ID has been passed")
viewModel.loadDriverApprovals(driverId)
}
override fun onSuccess(data: Any?) {
super.onSuccess(data)
@Suppress("UNCHECKED_CAST")
if (data is Map<*, *>) {
if (data is List<*>) {
val listData = data as List<Pair<String, ApprovalStatus>>
if (listView.adapter == null) {
listView.adapter = ApprovalListAdapter(layoutInflater, data as Map<String, Int?>) {
this.view?.applyNavigation(it)
listView.adapter = ApprovalListAdapter(requireContext(), listData) {
this.view?.navigateTo(
R.id.to_approverFragment,
driverId.toBundle(USER_CONST).apply { putString(FRAGMENT, it) })
}
listView.isScrollContainer = false
} else {
(listView.adapter as ApprovalListAdapter).updateAdapter(data as Map<String, Int?>)
(listView.adapter as ApprovalListAdapter).updateAdapter(listData)
}
}
}
private fun View.applyNavigation(key: String) {
val navId = when (key) {
context.getString(R.string.driver_profile) -> R.id.to_driverProfileFragment
context.getString(R.string.drivers_license) -> R.id.to_driverLicenseFragment
context.getString(R.string.private_hire_license) -> R.id.to_privateHireLicenseFragment
context.getString(R.string.vehicle_profile) -> R.id.to_vehicleProfileFragment
context.getString(R.string.insurance) -> R.id.to_insuranceFragment
context.getString(R.string.m_o_t) -> R.id.to_motFragment
context.getString(R.string.log_book) -> R.id.to_logbookFragment
context.getString(R.string.private_hire_vehicle_license) -> R.id.to_privateHireVehicleFragment
else -> {
throw StringIndexOutOfBoundsException("No resource for $key")
}
}
navigateTo(navId, driverId.toBundle(USER_CONST))
}
}

View File

@@ -105,10 +105,8 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
override fun onDataChanged() {
super.onDataChanged()
applyBinding {
// If there are no chat messages, show a view that invites the user to add a message.
if (itemCount == 0) {
emptyView.root.visibility = if (itemCount == 0) View.VISIBLE else View.GONE
}
// If there are no driver data, show a view that informs the admin.
emptyView.root.visibility = if (itemCount == 0) View.VISIBLE else View.GONE
progressCircular.hide()
}
}
@@ -124,7 +122,7 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
}
override fun connectionLost() {
requireContext().displayToast("No connection available")
showToast("No connection available")
}
}
}
@@ -134,7 +132,7 @@ class HomeSuperUserFragment : BaseFragment<SuperUserViewModel, FragmentHomeSuper
setTag(R.string.driver_identifier, "DriverIdentifierInput")
setText(defaultNumber)
setSelectAllOnFocus(true)
doOnTextChanged { _, _, count, _ -> if (count > 6) context.displayToast("Identifier cannot be larger than 6") }
doOnTextChanged { _, _, count, _ -> if (count > 6) showToast("Identifier cannot be larger than 6") }
}
val layout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL

View File

@@ -12,6 +12,7 @@ class VehicleProfileFragment :
override fun setupView(binding: FragmentVehicleSetupBinding) {
super.setupView(binding)
viewsToHide(binding.submit)
binding.seizedCheckbox.isEnabled = false
}
override fun setFields(data: VehicleProfile) {

View File

@@ -0,0 +1,3 @@
package h_mal.appttude.com.driver.utils
const val FRAGMENT = "fragment"

View File

@@ -0,0 +1,100 @@
package h_mal.appttude.com.driver.viewmodels
import android.content.res.Resources
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.google.firebase.database.DatabaseReference
import h_mal.appttude.com.driver.R
import h_mal.appttude.com.driver.base.BaseViewModel
import h_mal.appttude.com.driver.data.FirebaseCompletion
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.data.USER_CONST
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.objects.ApprovalsObject
import h_mal.appttude.com.driver.ui.driverprofile.DriverLicenseFragment
import h_mal.appttude.com.driver.ui.driverprofile.DriverProfileFragment
import h_mal.appttude.com.driver.ui.driverprofile.PrivateHireLicenseFragment
import h_mal.appttude.com.driver.ui.vehicleprofile.*
import h_mal.appttude.com.driver.utils.Coroutines.io
import h_mal.appttude.com.driver.utils.FRAGMENT
import h_mal.appttude.com.driver.utils.getDataFromDatabaseRef
class ApproverViewModel(
private val resources: Resources,
private val database: FirebaseDatabaseSource
) : BaseViewModel() {
private lateinit var name: String
private lateinit var docRef: DatabaseReference
private var score: ApprovalStatus? = null
fun init(args: Bundle) {
// Retried uid & fragment class name from args
val uid = args.getString(USER_CONST) ?: throw NullPointerException("No user Id was passed")
name = args.getString(FRAGMENT)
?: throw NullPointerException("No fragment name argument passed")
// Define a document name based on fragment class name
val documentName = when (name) {
resources.getString(R.string.driver_profile) -> ApprovalsObject::driver_details_approval.name
resources.getString(R.string.drivers_license) -> ApprovalsObject::driver_license_approval.name
resources.getString(R.string.private_hire_license) -> ApprovalsObject::private_hire_approval.name
resources.getString(R.string.vehicle_profile) -> ApprovalsObject::vehicle_details_approval.name
resources.getString(R.string.insurance) -> ApprovalsObject::insurance_details_approval.name
resources.getString(R.string.m_o_t) -> ApprovalsObject::mot_details_approval.name
resources.getString(R.string.log_book) -> ApprovalsObject::log_book_approval.name
resources.getString(R.string.private_hire_vehicle_license) -> ApprovalsObject::ph_car_approval.name
else -> {
throw StringIndexOutOfBoundsException("No resource for $name")
}
}
docRef = database.getDocumentApprovalRef(uid, documentName)
io {
doTryOperation("") {
val data = docRef.getDataFromDatabaseRef<Int>()
score = data?.let { ApprovalStatus.getByScore(it) } ?: ApprovalStatus.NOT_SUBMITTED
onSuccess(FirebaseCompletion.Default)
}
}
}
fun getFragmentClass(): Class<out Fragment> {
return when (name) {
resources.getString(R.string.driver_profile) -> DriverProfileFragment::class.java
resources.getString(R.string.drivers_license) -> DriverLicenseFragment::class.java
resources.getString(R.string.private_hire_license) -> PrivateHireLicenseFragment::class.java
resources.getString(R.string.vehicle_profile) -> VehicleProfileFragment::class.java
resources.getString(R.string.insurance) -> InsuranceFragment::class.java
resources.getString(R.string.m_o_t) -> MotFragment::class.java
resources.getString(R.string.log_book) -> LogbookFragment::class.java
resources.getString(R.string.private_hire_vehicle_license) -> PrivateHireVehicleFragment::class.java
else -> {
throw StringIndexOutOfBoundsException("No resource for $name")
}
}
}
fun approveDocument() {
updateDocument(ApprovalStatus.APPROVED)
}
fun declineDocument() {
updateDocument(ApprovalStatus.DENIED)
}
private fun updateDocument(approval: ApprovalStatus) {
if (approval == score) {
val result = if (approval == ApprovalStatus.APPROVED) "approved" else "declined"
onError("Document already $result")
return
}
io {
doTryOperation("Failed to decline document") {
database.postToDatabaseRed(docRef, approval.score)
onSuccess(approval)
}
}
}
}

View File

@@ -5,10 +5,12 @@ import com.google.firebase.storage.StorageReference
import h_mal.appttude.com.driver.base.DataSubmissionBaseViewModel
import h_mal.appttude.com.driver.data.FirebaseAuthentication
import h_mal.appttude.com.driver.data.FirebaseDatabaseSource
import h_mal.appttude.com.driver.model.ApprovalStatus
import h_mal.appttude.com.driver.objects.ApprovalsObject
import h_mal.appttude.com.driver.utils.Coroutines.io
import h_mal.appttude.com.driver.utils.getDataFromDatabaseRef
import kotlinx.coroutines.Job
import java.io.IOException
class DriverOverviewViewModel(
auth: FirebaseAuthentication,
@@ -37,17 +39,23 @@ class DriverOverviewViewModel(
}
}
private fun mapApprovalsForView(data: ApprovalsObject): Map<String, Int> {
return mutableMapOf<String, Int>().apply {
put("Driver Profile", data.driver_details_approval)
put("Drivers License", data.driver_license_approval)
put("Private Hire License", data.private_hire_approval)
put("Vehicle Profile", data.vehicle_details_approval)
put("Insurance", data.insurance_details_approval)
put("M.O.T", data.mot_details_approval)
put("Log book", data.log_book_approval)
put("Private Hire Vehicle License", data.ph_car_approval)
}.toMap()
private fun mapApprovalsForView(data: ApprovalsObject): List<Pair<String, ApprovalStatus>> {
val list = mutableListOf<Pair<String, ApprovalStatus>>()
return list.apply {
add(0, Pair("Driver Profile", getApprovalStatusByScore(data.driver_details_approval)))
add(1, Pair("Drivers License", getApprovalStatusByScore(data.driver_license_approval)))
add(2, Pair("Private Hire License", getApprovalStatusByScore(data.private_hire_approval)))
add(3, Pair("Vehicle Profile", getApprovalStatusByScore(data.vehicle_details_approval)))
add(4, Pair("Insurance", getApprovalStatusByScore(data.insurance_details_approval)))
add(5, Pair("M.O.T", getApprovalStatusByScore(data.mot_details_approval)))
add(6, Pair("Log book", getApprovalStatusByScore(data.log_book_approval)))
add(7, Pair("Private Hire Vehicle License", getApprovalStatusByScore(data.ph_car_approval)))
}
}
private fun getApprovalStatusByScore(score: Int): ApprovalStatus {
if (score == 0) return ApprovalStatus.NOT_SUBMITTED
return ApprovalStatus.getByScore(score) ?: throw IOException("No approval for score $score")
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ui.ApproverFragment">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonContainer"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_driver_license" />
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<com.google.android.material.button.MaterialButton
android:id="@+id/approve"
android:layout_marginLeft="12dp"
android:layout_marginRight="6dp"
android:layout_width="0dp"
android:layout_weight="1"
style="@style/TextButton.WithIcon"
app:icon="@drawable/baseline_check_24"
android:text="@string/approve" />
<com.google.android.material.button.MaterialButton
android:id="@+id/decline"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginLeft="6dp"
android:layout_marginRight="12dp"
style="@style/TextButton.WithIcon"
app:icon="@drawable/baseline_clear_24"
app:iconTint="@android:color/holo_red_light"
android:backgroundTint="@color/colour_ten"
android:text="@string/decline" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -23,68 +23,16 @@
android:id="@+id/userMainFragment"
android:name="h_mal.appttude.com.driver.ui.DriverOverviewFragment"
android:label="fragment_user_main"
tools:layout="@layout/fragment_user_main" >
tools:layout="@layout/fragment_user_main">
<action
android:id="@+id/to_driverLicenseFragment"
app:destination="@id/driverLicenseFragment" />
<action
android:id="@+id/to_driverProfileFragment"
app:destination="@id/driverProfileFragment" />
<action
android:id="@+id/to_privateHireLicenseFragment"
app:destination="@id/privateHireLicenseFragment" />
<action
android:id="@+id/to_insuranceFragment"
app:destination="@id/insuranceFragment" />
<action
android:id="@+id/to_logbookFragment"
app:destination="@id/logbookFragment" />
<action
android:id="@+id/to_motFragment"
app:destination="@id/motFragment" />
<action
android:id="@+id/to_privateHireVehicleFragment"
app:destination="@id/privateHireVehicleFragment" />
<action
android:id="@+id/to_vehicleProfileFragment"
app:destination="@id/vehicleProfileFragment" />
android:id="@+id/to_approverFragment"
app:destination="@id/approverFragment" />
</fragment>
<fragment
android:id="@+id/driverLicenseFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.DriverLicenseFragment"
android:label="DriverLicenseFragment" />
<fragment
android:id="@+id/driverProfileFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.DriverProfileFragment"
android:label="fragment_driver_profile"
tools:layout="@layout/fragment_driver_profile" />
<fragment
android:id="@+id/privateHireLicenseFragment"
android:name="h_mal.appttude.com.driver.ui.driverprofile.PrivateHireLicenseFragment"
android:label="fragment_private_hire_license"
tools:layout="@layout/fragment_private_hire_license" />
<fragment
android:id="@+id/insuranceFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.InsuranceFragment"
android:label="InsuranceFragment" />
<fragment
android:id="@+id/logbookFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.LogbookFragment"
android:label="fragment_logbook"
tools:layout="@layout/fragment_logbook" />
<fragment
android:id="@+id/motFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.MotFragment"
android:label="MotFragment" />
<fragment
android:id="@+id/privateHireVehicleFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.PrivateHireVehicleFragment"
android:label="fragment_private_hire_vehicle"
tools:layout="@layout/fragment_private_hire_vehicle" />
<fragment
android:id="@+id/vehicleProfileFragment"
android:name="h_mal.appttude.com.driver.ui.vehicleprofile.VehicleProfileFragment"
android:label="fragment_vehicle_setup"
tools:layout="@layout/fragment_vehicle_setup" />
android:id="@+id/approverFragment"
android:name="h_mal.appttude.com.driver.ui.ApproverFragment"
android:label="fragment_approver"
tools:layout="@layout/fragment_approver" />
</navigation>

View File

@@ -1,3 +1,5 @@
<resources>
<string name="app_name">Driver Admin</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources>