From 815852ca98b327cd96a1f59edfb311b679b030f5 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Fri, 25 Aug 2023 17:43:16 +0100 Subject: [PATCH 01/15] - update git ignore --- .gitignore | 102 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 39fb081..87e70e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,97 @@ -*.iml +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files .gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later .externalNativeBuild + +# IntelliJ IDEA +*.iml +*.iws +/out/ + +# User-specific configurations +.idea/androidTestResultsUserPreferences.xml +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositorie + +# Gem/fastlane +Gemfile.lock +/fastlane/report.xml +# Google play files +/google-play-key.json From cd20315b327897edfbbadb1f825dd6b9dee3cd6d Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Fri, 25 Aug 2023 17:44:54 +0100 Subject: [PATCH 02/15] - MVVM refactor - Kodein DI added - Overhaul dirty code in views --- app/build.gradle | 15 + .../h_mal/farmr/data/ShiftProviderTest.kt | 29 +- app/src/main/AndroidManifest.xml | 9 +- .../h_mal/farmr/FilterDataFragment.kt | 144 ----- .../appttude/h_mal/farmr/FragmentAddItem.kt | 507 ------------------ .../com/appttude/h_mal/farmr/FragmentMain.kt | 459 ---------------- .../h_mal/farmr/FurtherInfoFragment.kt | 161 ------ .../com/appttude/h_mal/farmr/MainActivity.kt | 116 ---- .../h_mal/farmr/ShiftsCursorAdapter.kt | 117 ---- .../h_mal/farmr/base/BackPressedListener.kt | 5 + .../appttude/h_mal/farmr/base/BaseActivity.kt | 58 ++ .../appttude/h_mal/farmr/base/BaseFragment.kt | 79 +++ .../h_mal/farmr/base/BaseRecyclerAdapter.kt | 47 ++ .../h_mal/farmr/base/BaseViewModel.kt | 26 + .../appttude/h_mal/farmr/data/Repository.kt | 24 + .../h_mal/farmr/data/RepositoryImpl.kt | 71 +++ .../farmr/data/legacydb/LegacyDatabase.kt | 167 ++++++ .../h_mal/farmr/data/legacydb/ShiftObject.kt | 28 + .../data/{ => legacydb}/ShiftProvider.kt | 6 +- .../data/{ => legacydb}/ShiftsContract.kt | 2 +- .../data/{ => legacydb}/ShiftsDbHelper.kt | 4 +- .../farmr/data/prefs/PreferencesProvider.kt | 65 +++ .../h_mal/farmr/di/ShiftApplication.kt | 28 + .../h_mal/farmr/model/DatabaseShift.kt | 11 + .../appttude/h_mal/farmr/model/FilterStore.kt | 8 + .../com/appttude/h_mal/farmr/model/Order.kt | 5 + .../com/appttude/h_mal/farmr/model/Shift.kt | 76 ++- .../appttude/h_mal/farmr/model/ShiftType.kt | 14 +- .../appttude/h_mal/farmr/model/Sortable.kt | 11 + .../appttude/h_mal/farmr/model/ViewState.kt | 7 + .../h_mal/farmr/ui/FilterDataFragment.kt | 92 ++++ .../h_mal/farmr/ui/FragmentAddItem.kt | 229 ++++++++ .../appttude/h_mal/farmr/ui/FragmentMain.kt | 367 +++++++++++++ .../h_mal/farmr/ui/FurtherInfoFragment.kt | 111 ++++ .../appttude/h_mal/farmr/ui/MainActivity.kt | 94 ++++ .../h_mal/farmr/ui/ShiftRecyclerAdapter.kt | 104 ++++ .../h_mal/farmr/{ => ui}/SplashScreen.kt | 3 +- .../appttude/h_mal/farmr/utils/Constants.kt | 7 + .../appttude/h_mal/farmr/utils/Formatting.kt | 81 +++ .../h_mal/farmr/utils/GenericsUtil.kt | 38 ++ .../appttude/h_mal/farmr/utils/ViewUtils.kt | 177 ++++++ .../viewmodel/ApplicationViewModelFactory.kt | 22 + .../h_mal/farmr/viewmodel/MainViewModel.kt | 483 +++++++++++++++++ app/src/main/res/layout/empty_list_view.xml | 38 ++ app/src/main/res/layout/fragment_add_item.xml | 13 +- .../main/res/layout/fragment_filter_data.xml | 2 +- .../main/res/layout/fragment_futher_info.xml | 2 +- app/src/main/res/layout/fragment_main.xml | 40 +- app/src/main/res/layout/list_item_1.xml | 1 - app/src/main/res/layout/main_view.xml | 2 +- app/src/main/res/menu/menu_main.xml | 2 +- app/src/main/res/values/strings.xml | 2 + 52 files changed, 2618 insertions(+), 1591 deletions(-) delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt delete mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt rename app/src/main/java/com/appttude/h_mal/farmr/data/{ => legacydb}/ShiftProvider.kt (97%) rename app/src/main/java/com/appttude/h_mal/farmr/data/{ => legacydb}/ShiftsContract.kt (96%) rename app/src/main/java/com/appttude/h_mal/farmr/data/{ => legacydb}/ShiftsDbHelper.kt (95%) create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt rename app/src/main/java/com/appttude/h_mal/farmr/{ => ui}/SplashScreen.kt (94%) create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt create mode 100644 app/src/main/res/layout/empty_list_view.xml diff --git a/app/build.gradle b/app/build.gradle index 33df36f..8a92332 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 31 @@ -29,8 +30,22 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.fragment:fragment-ktx:1.4.0' + implementation 'androidx.activity:activity-ktx:1.4.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' + implementation 'androidx.preference:preference:1.2.1' + implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' + / * Room database * / + def room_version = "2.4.3" + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + / *Kodein Dependency Injection * / + def kodein_version = "6.2.1" + implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" + implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt index d8872a5..67a4c51 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt @@ -3,19 +3,20 @@ package com.appttude.h_mal.farmr.data import android.content.ContentResolver import android.content.ContentValues import androidx.test.rule.provider.ProviderTestRule -import com.appttude.h_mal.farmr.data.ShiftsContract.CONTENT_AUTHORITY -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.CONTENT_URI -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.CONTENT_AUTHORITY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull import org.junit.Rule @@ -75,6 +76,8 @@ class ShiftProviderTest { // Assert val item = contentResolver.query(CONTENT_URI, projection, null, null, null) item?.takeIf { it.moveToNext() }?.run { + val id = getLong(getColumnIndexOrThrow(_ID)) + val descriptionColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DESCRIPTION)) val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE)) val timeInColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8dc1d6f..12b8ee2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt deleted file mode 100644 index 78945fb..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.DatePickerDialog -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.Button -import android.widget.EditText -import android.widget.Spinner -import androidx.fragment.app.Fragment -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import java.text.MessageFormat -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date - -class FilterDataFragment : Fragment() { - private val spinnerList: Array = arrayOf("", "Hourly", "Piece Rate") - private val listArgs: MutableList = ArrayList() - private var LocationET: EditText? = null - private var dateFromET: EditText? = null - private var dateToET: EditText? = null - private var typeSpinner: Spinner? = null - lateinit var mcurrentDate: Calendar - private lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - val activity = (activity) - - // Inflate the layout for this fragment - val rootView: View = inflater.inflate(R.layout.fragment_filter_data, container, false) - activity.setActionBarTitle(getString(R.string.title_activity_filter_data)) - mcurrentDate = Calendar.getInstance() - LocationET = rootView.findViewById(R.id.filterLocationEditText) as EditText? - dateFromET = rootView.findViewById(R.id.fromdateInEditText) as EditText? - dateToET = rootView.findViewById(R.id.filterDateOutEditText) as EditText? - typeSpinner = rootView.findViewById(R.id.TypeFilterEditText) as Spinner? - - - if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?")) { - var str: String = activity.args!!.get(2) - str = str.replace("%", "") - LocationET!!.setText(str) - } - if (activity.selection != null && !(activity.args!!.get(0) == "2000-01-01")) { - dateFromET!!.setText(activity.args!!.get(0)) - } - if ((activity.selection != null) && (activity.args != null) && !(activity.args!![1] == (mcurrentDate.get(Calendar.YEAR).toString() + "-" - + String.format("%02d", (mcurrentDate.get(Calendar.MONTH) + 1)) + "-" - + String.format("%02d", mcurrentDate.get(Calendar.DAY_OF_MONTH))).toString())) { - dateToET!!.setText(activity.args!![1]) - } - dateFromET!!.setOnClickListener { //To show current date in the datepicker - var mYear: Int = mcurrentDate.get(Calendar.YEAR) - var mMonth: Int = mcurrentDate.get(Calendar.MONTH) - var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH) - if (!(dateFromET!!.text.toString() == "")) { - val dateString: String = dateFromET!!.text.toString().trim() - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth = mMonth - 1 - } - mDay = dateString.substring(8).toInt() - } - - val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday -> - val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday)) - dateFromET!!.setText(input) - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - dateToET!!.setOnClickListener { //To show current date in the datepicker - val mcurrentDate: Calendar = Calendar.getInstance() - var mYear: Int = mcurrentDate.get(Calendar.YEAR) - var mMonth: Int = mcurrentDate.get(Calendar.MONTH) - var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH) - if (!(dateToET!!.text.toString() == "")) { - val dateString: String = dateToET!!.text.toString().trim({ it <= ' ' }) - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth -= 1 - } - mDay = dateString.substring(8).toInt() - } - val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday -> - val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday)) - dateToET!!.setText(input) - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - val adapter: ArrayAdapter = ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList) - typeSpinner!!.adapter = adapter - if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?")) { - val spinnerPosition: Int = adapter.getPosition(activity.args!!.get(activity.args!!.size - 1)) - typeSpinner!!.setSelection(spinnerPosition) - } - val submit: Button = rootView.findViewById(R.id.submitFiltered) as Button - submit.setOnClickListener { - BuildQuery() - activity.args = listArgs.toTypedArray() - FragmentMain.NEW_LOADER = 1 - activity.fragmentManager!!.popBackStack() - } - return rootView - } - - private fun BuildQuery() { - val dateQuery: String = ShiftsEntry.COLUMN_SHIFT_DATE + " BETWEEN ? AND ?" - val descQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?" - val typeQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?" - var dateFrom = "2000-01-01" - val c: Date = Calendar.getInstance().time - val df = SimpleDateFormat("yyyy-MM-dd") - var dateTo: String = df.format(c) - if (!TextUtils.isEmpty(dateFromET!!.text.toString().trim())) { - dateFrom = dateFromET!!.text.toString().trim() - } - if (!TextUtils.isEmpty(dateToET!!.text.toString().trim())) { - dateTo = dateToET!!.text.toString().trim() - } - activity.selection = dateQuery - listArgs.add(dateFrom) - listArgs.add(dateTo) - if (!TextUtils.isEmpty(LocationET!!.text.toString().trim())) { - activity.selection = activity.selection + descQuery - listArgs.add("%" + LocationET!!.text.toString().trim() + "%") - } - if (!(typeSpinner!!.selectedItem.toString() == "")) { - activity.selection = activity.selection + typeQuery - listArgs.add(typeSpinner!!.selectedItem.toString()) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt deleted file mode 100644 index eedcee5..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt +++ /dev/null @@ -1,507 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.DatePickerDialog -import android.app.DatePickerDialog.OnDateSetListener -import android.app.TimePickerDialog -import android.app.TimePickerDialog.OnTimeSetListener -import android.content.ContentValues -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.DatePicker -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.ScrollView -import android.widget.TextView -import android.widget.TimePicker -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import java.util.Calendar - -class FragmentAddItem : Fragment(), LoaderManager.LoaderCallbacks { - lateinit var activity: MainActivity - - private var mCurrentProductUri: Uri? = null - private var mRadioButtonOne: RadioButton? = null - private var mRadioButtonTwo: RadioButton? = null - private var mLocationEditText: EditText? = null - private var mDateEditText: EditText? = null - private var mDurationTextView: TextView? = null - private var mTimeInEditText: EditText? = null - private var mTimeOutEditText: EditText? = null - private var mBreakEditText: EditText? = null - private var mUnitEditText: EditText? = null - private var mPayRateEditText: EditText? = null - private var mTotalPayTextView: TextView? = null - private var hourlyDataView: LinearLayout? = null - private var unitsHolder: LinearLayout? = null - private var durationHolder: LinearLayout? = null - private var wholeView: LinearLayout? = null - private var scrollView: ScrollView? = null - private var progressBarAI: ProgressBar? = null - private var mBreaks = 0 - private var mDay = 0 - private var mMonth = 0 - private var mYear = 0 - private var mHoursIn = 0 - private var mMinutesIn = 0 - private var mHoursOut = 0 - private var mMinutesOut = 0 - private var mUnits = 0f - private var mPayRate = 0f - private var mType: String? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - val rootView = inflater.inflate(R.layout.fragment_add_item, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - - progressBarAI = rootView.findViewById(R.id.pd_ai) as ProgressBar - scrollView = rootView.findViewById(R.id.total_view) as ScrollView - mRadioGroup = rootView.findViewById(R.id.rg) as RadioGroup - mRadioButtonOne = rootView.findViewById(R.id.hourly) as RadioButton - mRadioButtonTwo = rootView.findViewById(R.id.piecerate) as RadioButton - mLocationEditText = rootView.findViewById(R.id.locationEditText) as EditText - mDateEditText = rootView.findViewById(R.id.dateEditText) as EditText - mTimeInEditText = rootView.findViewById(R.id.timeInEditText) as EditText - mBreakEditText = rootView.findViewById(R.id.breakEditText) as EditText - mTimeOutEditText = rootView.findViewById(R.id.timeOutEditText) as EditText - mDurationTextView = rootView.findViewById(R.id.ShiftDuration) as TextView - mUnitEditText = rootView.findViewById(R.id.unitET) as EditText - mPayRateEditText = rootView.findViewById(R.id.payrateET) as EditText - mTotalPayTextView = rootView.findViewById(R.id.totalpayval) as TextView - hourlyDataView = rootView.findViewById(R.id.hourly_data_holder) as LinearLayout - unitsHolder = rootView.findViewById(R.id.units_holder) as LinearLayout - durationHolder = rootView.findViewById(R.id.duration_holder) as LinearLayout - wholeView = rootView.findViewById(R.id.whole_view) as LinearLayout - mPayRate = 0.0f - mUnits = 0.0f - val b = arguments - if (b != null) { - mCurrentProductUri = Uri.parse(b.getString("uri")) - } - if (mCurrentProductUri == null) { - activity.setActionBarTitle(getString(R.string.add_item_title)) - wholeView!!.visibility = View.GONE - } else { - activity.setActionBarTitle(getString(R.string.edit_item_title)) - loaderManager.initLoader(EXISTING_PRODUCT_LOADER, null, this) - } - mBreakEditText!!.hint = "insert break in minutes" - mRadioGroup!!.setOnCheckedChangeListener(RadioGroup.OnCheckedChangeListener { radioGroup, i -> - if (mRadioButtonOne!!.isChecked) { - mType = mRadioButtonOne!!.text.toString() - wholeView!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.GONE - hourlyDataView!!.visibility = View.VISIBLE - durationHolder!!.visibility = View.VISIBLE - } else if (mRadioButtonTwo!!.isChecked) { - mType = mRadioButtonTwo!!.text.toString() - wholeView!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.VISIBLE - hourlyDataView!!.visibility = View.GONE - durationHolder!!.visibility = View.GONE - } - }) - mDateEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - //To show current date in the datepicker - if (TextUtils.isEmpty(mDateEditText!!.text.toString().trim { it <= ' ' })) { - val mcurrentDate = Calendar.getInstance() - mYear = mcurrentDate[Calendar.YEAR] - mMonth = mcurrentDate[Calendar.MONTH] - mDay = mcurrentDate[Calendar.DAY_OF_MONTH] - } else { - val dateString = mDateEditText!!.text.toString().trim { it <= ' ' } - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth = mMonth - 1 - } - mDay = dateString.substring(8).toInt() - } - val mDatePicker = DatePickerDialog((context)!!, object : OnDateSetListener { - override fun onDateSet(datepicker: DatePicker, selectedyear: Int, selectedmonth: Int, selectedday: Int) { - var selectedmonth = selectedmonth - mDateEditText!!.setText( - (selectedyear.toString() + "-" - + String.format("%02d", (selectedmonth + 1.also { selectedmonth = it })) + "-" - + String.format("%02d", selectedday)) - ) - setDate(selectedyear, selectedmonth, selectedday) - } - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - }) - mTimeInEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - if ((mTimeInEditText!!.text.toString() == "")) { - val mcurrentTime = Calendar.getInstance() - mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesIn = mcurrentTime[Calendar.MINUTE] - } else { - mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - val mTimePicker: TimePickerDialog - mTimePicker = TimePickerDialog(context, object : OnTimeSetListener { - override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) { - val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute) - setTime(selectedMinute, selectedHour) - mTimeInEditText!!.setText(ddTime) - } - }, mHoursIn, mMinutesIn, true) //Yes 24 hour time - mTimePicker.setTitle("Select Time") - mTimePicker.show() - } - }) - mTimeOutEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - if ((mTimeOutEditText!!.text.toString() == "")) { - val mcurrentTime = Calendar.getInstance() - mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesOut = mcurrentTime[Calendar.MINUTE] - } else { - mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - val mTimePicker: TimePickerDialog - mTimePicker = TimePickerDialog(context, object : OnTimeSetListener { - override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) { - val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute) - setTime2(selectedMinute, selectedHour) - mTimeOutEditText!!.setText(ddTime) - } - }, mHoursOut, mMinutesOut, true) //Yes 24 hour time - mTimePicker.setTitle("Select Time") - mTimePicker.show() - } - }) - mTimeInEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mTimeOutEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mBreakEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mUnitEditText!!.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun afterTextChanged(editable: Editable) { -// if(mRadioButtonTwo.isChecked()) { -// mUnits = 0.0f; -// if (!mUnitEditText.getText().toString().equals("")){ -// mUnits = Float.parseFloat(mUnitEditText.getText().toString()); -// } -// mPayRate = 0.0f; -// if (!mPayRateEditText.getText().toString().equals("")){ -// mPayRate = Float.parseFloat(mPayRateEditText.getText().toString()); -// } -// Float total = mPayRate * mUnits; -// mTotalPayTextView.setText(String.valueOf(total)); -// } - calculateTotalPay() - } - }) - mPayRateEditText!!.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun afterTextChanged(editable: Editable) { - calculateTotalPay() - } - }) - val SubmitProduct = rootView.findViewById(R.id.submit) as Button - SubmitProduct.setOnClickListener(object : View.OnClickListener { - override fun onClick(view: View) { - saveProduct() - } - }) - return rootView - } - - private fun calculateTotalPay() { - var total = 0.0f - mPayRate = 0.0f - if (mRadioButtonTwo!!.isChecked) { - mUnits = 0.0f - if (mUnitEditText!!.text.toString() != "") { - mUnits = mUnitEditText!!.text.toString().toFloat() - } - if (mPayRateEditText!!.text.toString() != "") { - mPayRate = mPayRateEditText!!.text.toString().toFloat() - } - total = mPayRate * mUnits - mTotalPayTextView!!.text = total.toString() - } else if (mRadioButtonOne!!.isChecked) { - if (mPayRateEditText!!.text.toString() != "") { - mPayRate = mPayRateEditText!!.text.toString().toFloat() - total = mPayRate * calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks) - } - } - mTotalPayTextView!!.text = String.format("%.2f", total) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - menu.clear() - } - - private fun saveProduct() { - val typeString = mType - val descriptionString = mLocationEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(descriptionString)) { - Toast.makeText(context, "please insert Location", Toast.LENGTH_SHORT).show() - return - } - val dateString = mDateEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(dateString)) { - Toast.makeText(context, "please insert Date", Toast.LENGTH_SHORT).show() - return - } - var timeInString: String = "" - var timeOutString: String = "" - var breaks = 0 - var units = 0f - var duration = 0f - var payRate = 0f - val payRateString = mPayRateEditText!!.text.toString().trim { it <= ' ' } - if (!TextUtils.isEmpty(payRateString)) { - payRate = payRateString.toFloat() - } - var totalPay = 0f - if ((typeString == "Hourly")) { - timeInString = mTimeInEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(timeInString)) { - Toast.makeText(context, "please insert Time in", Toast.LENGTH_SHORT).show() - return - } - timeOutString = mTimeOutEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(timeOutString)) { - Toast.makeText(context, "please insert Time out", Toast.LENGTH_SHORT).show() - return - } - val breakMins = mBreakEditText!!.text.toString().trim { it <= ' ' } - if (!TextUtils.isEmpty(breakMins)) { - breaks = breakMins.toInt() - if ((breaks.toFloat() / 60) > calculateDurationWithoutBreak(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut)) { - Toast.makeText(context, "Break larger than duration", Toast.LENGTH_SHORT).show() - return - } - } - duration = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, breaks) - totalPay = duration * payRate - } else if ((typeString == "Piece Rate")) { - val unitsString = mUnitEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(unitsString) || unitsString.toFloat() <= 0) { - Toast.makeText(context, "Insert Units", Toast.LENGTH_SHORT).show() - return - } else { - units = unitsString.toFloat() - } - duration = 0f - totalPay = units * payRate - } - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeString) - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionString) - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateString) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInString) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutString) - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, duration) - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breaks) - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, units) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payRate) - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) - if (mCurrentProductUri == null) { - val newUri = activity.contentResolver.insert(ShiftsEntry.CONTENT_URI, values) - if (newUri == null) { - Toast.makeText(context, getString(R.string.insert_item_failed), - Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, getString(R.string.insert_item_successful), - Toast.LENGTH_SHORT).show() - } - } else { - val rowsAffected = activity.contentResolver.update(mCurrentProductUri!!, values, null, null) - if (rowsAffected == 0) { - Toast.makeText(context, getString(R.string.update_item_failed), - Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, getString(R.string.update_item_successful), - Toast.LENGTH_SHORT).show() - } - } - (activity.fragmentManager)!!.popBackStack("main", 0) - } - - private fun setDuration() { - val mcurrentTime = Calendar.getInstance() - mBreaks = 0 - if (mBreakEditText!!.text.toString() != "") { - mBreaks = mBreakEditText!!.text.toString().toInt() - } - if ((mTimeOutEditText!!.text.toString() == "")) { - mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesOut = mcurrentTime[Calendar.MINUTE] - } else { - mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - if ((mTimeInEditText!!.text.toString() == "")) { - mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesIn = mcurrentTime[Calendar.MINUTE] - } else { - mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - mDurationTextView!!.text = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks).toString() + " hours" - } - - private fun setDate(year: Int, month: Int, day: Int) { - mYear = year - mMonth = month - mDay = day - } - - private fun setTime(minutes: Int, hours: Int) { - mMinutesIn = minutes - mHoursIn = hours - } - - private fun setTime2(minutes: Int, hours: Int) { - mMinutesOut = minutes - mHoursOut = hours - } - - private fun calculateDuration(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int, breaks: Int): Float { - val duration: Float - if (hoursOut > hoursIn) { - duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60) - } else { - duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60) + 24) - } - val s = String.format("%.2f", duration) - return s.toFloat() - } - - private fun calculateDurationWithoutBreak(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int): Float { - val duration: Float - if (hoursOut > hoursIn) { - duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - } else { - duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24) - } - val s = String.format("%.2f", duration) - return s.toFloat() - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - progressBarAI!!.visibility = View.VISIBLE - scrollView!!.visibility = View.GONE - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader((context)!!, (mCurrentProductUri)!!, - projection, null, null, null) - } - - override fun onLoaderReset(loader: Loader) {} - - override fun onLoadFinished(loader: Loader, cursor: Cursor?) { - progressBarAI!!.visibility = View.GONE - scrollView!!.visibility = View.VISIBLE - if (cursor == null || cursor.count < 1) { - return - } - if (cursor.moveToFirst()) { - val descriptionColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - val dateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE) - val timeInColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - val timeOutColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - val breakColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK) - val durationColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION) - val typeColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE) - val unitColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT) - val payrateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE) - val totalPayColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val type = cursor.getString(typeColumnIndex) - val description = cursor.getString(descriptionColumnIndex) - val date = cursor.getString(dateColumnIndex) - val timeIn = cursor.getString(timeInColumnIndex) - val timeOut = cursor.getString(timeOutColumnIndex) - val breaks = cursor.getInt(breakColumnIndex) - val duration = cursor.getFloat(durationColumnIndex) - val unit = cursor.getFloat(unitColumnIndex) - val payrate = cursor.getFloat(payrateColumnIndex) - val totalPay = cursor.getFloat(totalPayColumnIndex) - mLocationEditText!!.setText(description) - mDateEditText!!.setText(date) - if ((type == "Hourly") || (type == "hourly")) { - mRadioButtonOne!!.isChecked = true - mRadioButtonTwo!!.isChecked = false - mTimeInEditText!!.setText(timeIn) - mTimeOutEditText!!.setText(timeOut) - mBreakEditText!!.setText(Integer.toString(breaks)) - mDurationTextView!!.text = String.format("%.2f", duration) + " Hours" - } else if ((type == "Piece Rate")) { - mRadioButtonOne!!.isChecked = false - mRadioButtonTwo!!.isChecked = true - mUnitEditText!!.setText(java.lang.Float.toString(unit)) - } - mPayRateEditText!!.setText(String.format("%.2f", payrate)) - mTotalPayTextView!!.text = String.format("%.2f", totalPay) - } - } - - companion object { - private val EXISTING_PRODUCT_LOADER = 0 - var mRadioGroup: RadioGroup? = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt deleted file mode 100644 index 2b09b09..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt +++ /dev/null @@ -1,459 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.app.AlertDialog -import android.content.ContentValues -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.PackageManager -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.os.StrictMode -import android.os.StrictMode.VmPolicy -import android.util.Log -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.ListView -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.ajts.androidmads.library.SQLiteToExcel -import com.ajts.androidmads.library.SQLiteToExcel.ExportListener -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import com.appttude.h_mal.farmr.data.ShiftsDbHelper -import com.google.android.material.floatingactionbutton.FloatingActionButton -import java.io.File - -class FragmentMain : Fragment(), LoaderManager.LoaderCallbacks { - var mCursorAdapter: ShiftsCursorAdapter? = null - var shiftsDbhelper: ShiftsDbHelper? = null - lateinit var defaultLoaderCallback: LoaderManager.LoaderCallbacks - lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - val rootView = inflater.inflate(R.layout.fragment_main, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - - activity.setActionBarTitle(getString(R.string.app_name)) - activity.filter = activity.getSharedPreferences("PREFS", 0) - activity.sortOrder = activity.filter?.getString("Filter", null) - defaultLoaderCallback = this - val productListView = rootView.findViewById(R.id.list_item_view) as ListView - val emptyView = rootView.findViewById(R.id.empty_view) - productListView.emptyView = emptyView - mCursorAdapter = ShiftsCursorAdapter(activity, null) - productListView.adapter = mCursorAdapter - loaderManager.initLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - val fab = rootView.findViewById(R.id.fab1) - fab.setOnClickListener { - val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction() - fragmentTransaction.replace(R.id.container, FragmentAddItem()).addToBackStack("additem").commit() - } - return rootView - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.delete_all -> { - deleteAllProducts() - return true - } - - R.id.help -> { - AlertDialog.Builder(context) - .setTitle("Help & Support:") - .setView(R.layout.dialog_layout) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show() - return true - } - - R.id.filter_data -> { - val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction() - fragmentTransaction.replace(R.id.container, FilterDataFragment()).addToBackStack("filterdata").commit() - return true - } - - R.id.sort_data -> { - sortData() - return true - } - - R.id.clear_filter -> { - activity.args = null - activity.selection = null - NEW_LOADER = 0 - loaderManager.restartLoader(DEFAULT_LOADER, null, this) - return true - } - - R.id.export_data -> { - if (checkStoragePermissions(activity)) { - AlertDialog.Builder(context) - .setTitle("Export?") - .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() - } else { - Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT).show() - } - return true - } - - R.id.action_favorite -> { - AlertDialog.Builder(context) - .setTitle("Info:") - .setMessage(retrieveInfo()) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show() - return true - } - } - return super.onOptionsItemSelected(item) - } - - private fun sortData() { - val grpname = arrayOf("Added", "Date", "Name") - val sortQuery = arrayOf("") - var checkedItem = -1 - if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry._ID)) { - checkedItem = 0 - sortQuery[0] = ShiftsEntry._ID - } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DATE)) { - checkedItem = 1 - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE - } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) { - checkedItem = 2 - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION - } - val alt_bld = AlertDialog.Builder(context) - //alt_bld.setIcon(R.drawable.icon); - alt_bld.setTitle("Sort by:") - alt_bld.setSingleChoiceItems(grpname, checkedItem, DialogInterface.OnClickListener { dialog, item -> - when (item) { - 0 -> { - sortQuery[0] = ShiftsEntry._ID - return@OnClickListener - } - - 1 -> { - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE - return@OnClickListener - } - - 2 -> sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION - } - }).setPositiveButton("Ascending") { dialog, id -> - activity.sortOrder = sortQuery[0] + " ASC" - activity.filter!!.edit().putString("Filter", activity.sortOrder).apply() - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - dialog.dismiss() - }.setNegativeButton("Descending") { dialog, id -> - activity.sortOrder = sortQuery[0] + " DESC" - activity.filter!!.edit().putString("Filter", activity.sortOrder).apply() - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - dialog.dismiss() - } - val alert = alt_bld.create() - alert.show() - } - - private fun deleteAllProducts() { - AlertDialog.Builder(context) - .setTitle("Delete?") - .setMessage("Are you sure you want to delete all date?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> - val rowsDeleted = activity.contentResolver.delete(ShiftsEntry.CONTENT_URI, null, null) - Toast.makeText(context, "$rowsDeleted Items Deleted", Toast.LENGTH_SHORT).show() - }.create().show() - } - - override fun onResume() { - super.onResume() - if (NEW_LOADER > DEFAULT_LOADER) { - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - println("reloading loader") - } - } - - private fun ExportData() { - val permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() - return - } - shiftsDbhelper = ShiftsDbHelper(context) - val database = shiftsDbhelper!!.writableDatabase - val projection_export = arrayOf( - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val cursor = activity.contentResolver.query( - ShiftsEntry.CONTENT_URI, - projection_export, - activity.selection, - activity.args, - activity.sortOrder) - database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null) - var totalDuration = 0.00f - var totalUnits = 0.00f - var totalPay = 0.00f - try { - while (cursor!!.moveToNext()) { - val descriptionColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) - val dateColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) - val timeInColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) - val timeOutColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) - val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val breakOutColumnIndex = cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK)) - val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val payrateColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE)) - val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - totalUnits = totalUnits + unitColumnIndex - totalDuration = totalDuration + durationColumnIndex - totalPay = totalPay + totalpayColumnIndex - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex) - database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) - } - } catch (e: Exception) { - Log.e("FragmentMain", "ExportData: ", e) - } finally { - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "") - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "") - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "") - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "") - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:") - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration) - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:") - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:") - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) - database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) - cursor!!.close() - } - val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp" - val file = File(savePath) - if (!file.exists()) { - file.mkdirs() - } - val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath) - sqLiteToExcel.exportSingleTable("shiftsexport", "shifthistory.xls", object : ExportListener { - override fun onStart() {} - override fun onCompleted(filePath: String) { - Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show() - val newPath = Uri.parse("file://$savePath/shifthistory.xls") - val builder = VmPolicy.Builder() - StrictMode.setVmPolicy(builder.build()) - val emailintent = Intent(Intent.ACTION_SEND) - emailintent.type = "application/vnd.ms-excel" - emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts") - emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.") - emailintent.putExtra(Intent.EXTRA_STREAM, newPath) - startActivity(Intent.createChooser(emailintent, "Send Email")) - } - - override fun onError(e: Exception) { - println("Error msg: $e") - Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show() - } - }) - } - - private fun retrieveInfo(): String { - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val cursor = activity.contentResolver.query( - ShiftsEntry.CONTENT_URI, - projection, - activity.selection, - activity.args, - activity.sortOrder) - var totalDuration = 0.0f - var countOfTypeH = 0 - var countOfTypeP = 0 - var totalUnits = 0f - var totalPay = 0f - var lines = 0 - try { - while (cursor!!.moveToNext()) { - val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - totalDuration = totalDuration + durationColumnIndex - if (typeColumnIndex.contains("Hourly")) { - countOfTypeH = countOfTypeH + 1 - } else if (typeColumnIndex.contains("Piece")) { - countOfTypeP = countOfTypeP + 1 - } - totalUnits = totalUnits + unitColumnIndex - totalPay = totalPay + totalpayColumnIndex - } - } finally { - if (cursor != null && cursor.count > 0) { - lines = cursor.count - cursor.close() - } - } - return buildInfoString(totalDuration, countOfTypeH, countOfTypeP, totalUnits, totalPay, lines) - } - - @SuppressLint("DefaultLocale") - fun buildInfoString(totalDuration: Float, countOfTypeH: Int, countOfTypeP: Int, totalUnits: Float, totalPay: Float, lines: Int): String { - var textString: String - textString = "$lines Shifts" - if (countOfTypeH != 0 && countOfTypeP != 0) { - textString = "$textString ($countOfTypeH Hourly/$countOfTypeP Piece Rate)" - } - if (countOfTypeH != 0) { - textString = """ - $textString - Total Hours: ${String.format("%.2f", totalDuration)} - """.trimIndent() - } - if (countOfTypeP != 0) { - textString = """ - $textString - Total Units: ${String.format("%.2f", totalUnits)} - """.trimIndent() - } - if (totalPay != 0f) { - textString = """ - $textString - Total Pay: ${"$"}${String.format("%.2f", totalPay)} - """.trimIndent() - } - return textString - } - - override fun onCreateLoader(i: Int, bundle: Bundle?): Loader { - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader(context!!, - ShiftsEntry.CONTENT_URI, - projection, - activity.selection, - activity.args, - activity.sortOrder) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor) { - mCursorAdapter!!.swapCursor(cursor) - } - - override fun onLoaderReset(loader: Loader) { - mCursorAdapter!!.swapCursor(null) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - println("request code$requestCode") - if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { - if (grantResults.size > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - exportDialog() - } else { - Toast.makeText(context, "Storage Permissions denied", Toast.LENGTH_SHORT).show() - } - } - } - - fun exportDialog() { - AlertDialog.Builder(context) - .setTitle("Export?") - .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() - } - - companion object { - const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 - private const val DEFAULT_LOADER = 0 - var NEW_LOADER = 0 - - // // Storage Permissions - // private static final int REQUEST_EXTERNAL_STORAGE = 1; - // private static String[] PERMISSIONS_STORAGE = { - // Manifest.permission.READ_EXTERNAL_STORAGE, - // Manifest.permission.WRITE_EXTERNAL_STORAGE - // }; - // /** - // * Checks if the app has permission to write to device storage - // * - // * If the app does not has permission then the user will be prompted to grant permissions - // * - // * @param activity - // */ - // public static void verifyStoragePermissions(Activity activity) { - // // Check if we have write permission - // int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); - // - // if (permission != PackageManager.PERMISSION_GRANTED) { - // // We don't have permission so prompt the user - // ActivityCompat.requestPermissions( - // activity, - // PERMISSIONS_STORAGE, - // REQUEST_EXTERNAL_STORAGE - // ); - // } - // } - fun checkStoragePermissions(activity: Activity?): Boolean { - var status = false - val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission == PackageManager.PERMISSION_GRANTED) { - status = true - } - return status - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt deleted file mode 100644 index eedc41d..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry - -class FurtherInfoFragment : Fragment(), LoaderManager.LoaderCallbacks { - private var typeTV: TextView? = null - private var descriptionTV: TextView? = null - private var dateTV: TextView? = null - private var times: TextView? = null - private var breakTV: TextView? = null - private var durationTV: TextView? = null - private var unitsTV: TextView? = null - private var payRateTV: TextView? = null - private var totalPayTV: TextView? = null - private var hourlyDetailHolder: LinearLayout? = null - private var unitsHolder: LinearLayout? = null - private var wholeView: LinearLayout? = null - private var progressBarFI: ProgressBar? = null - private var editButton: Button? = null - private var CurrentUri: Uri? = null - - lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - // Inflate the layout for this fragment - val rootView: View = inflater.inflate(R.layout.fragment_futher_info, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - activity.setActionBarTitle(getString(R.string.further_info_title)) - - progressBarFI = rootView.findViewById(R.id.progressBar_info) as ProgressBar? - wholeView = rootView.findViewById(R.id.further_info_view) as LinearLayout? - typeTV = rootView.findViewById(R.id.details_shift) as TextView? - descriptionTV = rootView.findViewById(R.id.details_desc) as TextView? - dateTV = rootView.findViewById(R.id.details_date) as TextView? - times = rootView.findViewById(R.id.details_time) as TextView? - breakTV = rootView.findViewById(R.id.details_breaks) as TextView? - durationTV = rootView.findViewById(R.id.details_duration) as TextView? - unitsTV = rootView.findViewById(R.id.details_units) as TextView? - payRateTV = rootView.findViewById(R.id.details_pay_rate) as TextView? - totalPayTV = rootView.findViewById(R.id.details_totalpay) as TextView? - editButton = rootView.findViewById(R.id.details_edit) as Button? - hourlyDetailHolder = rootView.findViewById(R.id.details_hourly_details) as LinearLayout? - unitsHolder = rootView.findViewById(R.id.details_units_holder) as LinearLayout? - val b: Bundle? = arguments - CurrentUri = Uri.parse(b!!.getString("uri")) - loaderManager.initLoader(DEFAULT_LOADER, null, this) - editButton!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(view: View) { - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment: Fragment = FragmentAddItem() - fragment.arguments = b - fragmentTransaction.replace(R.id.container, fragment).addToBackStack("additem").commit() - } - }) - return rootView - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - menu.clear() - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - progressBarFI!!.visibility = View.VISIBLE - wholeView!!.visibility = View.GONE - val projection: Array = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader((context)!!, (CurrentUri)!!, - projection, null, null, null) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor) { - progressBarFI!!.visibility = View.GONE - wholeView!!.visibility = View.VISIBLE - if (cursor == null || cursor.count < 1) { - return - } - if (cursor.moveToFirst()) { - val descriptionColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - val dateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE) - val timeInColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - val timeOutColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - val breakColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK) - val durationColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION) - val typeColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE) - val unitColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT) - val payrateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE) - val totalPayColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val type: String = cursor.getString(typeColumnIndex) - val description: String = cursor.getString(descriptionColumnIndex) - val date: String = cursor.getString(dateColumnIndex) - val timeIn: String = cursor.getString(timeInColumnIndex) - val timeOut: String = cursor.getString(timeOutColumnIndex) - val breaks: Int = cursor.getInt(breakColumnIndex) - val duration: Float = cursor.getFloat(durationColumnIndex) - val unit: Float = cursor.getFloat(unitColumnIndex) - val payrate: Float = cursor.getFloat(payrateColumnIndex) - val totalPay: Float = cursor.getFloat(totalPayColumnIndex) - var durationString: String = ShiftsCursorAdapter.Companion.timeValues(duration).get(0) + " Hours " + ShiftsCursorAdapter.Companion.timeValues(duration).get(1) + " Minutes " - if (breaks != 0) { - durationString = durationString + " (+ " + Integer.toString(breaks) + " minutes break)" - } - typeTV!!.text = type - descriptionTV!!.text = description - dateTV!!.text = date - var totalPaid: String? = "" - val currency: String = "$" - if ((type == "Hourly")) { - hourlyDetailHolder!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.GONE - times!!.text = timeIn + " - " + timeOut - breakTV!!.text = Integer.toString(breaks) + "mins" - durationTV!!.text = durationString - totalPaid = (String.format("%.2f", duration) + " Hours @ " + currency + String.format("%.2f", payrate) + " per Hour" + "\n" - + "Equals: " + currency + String.format("%.2f", totalPay)) - } else if ((type == "Piece Rate")) { - hourlyDetailHolder!!.visibility = View.GONE - unitsHolder!!.visibility = View.VISIBLE - unitsTV!!.text = String.format("%.2f", unit) - totalPaid = (String.format("%.2f", unit) + " Units @ " + currency + String.format("%.2f", payrate) + " per Unit" + "\n" - + "Equals: " + currency + String.format("%.2f", totalPay)) - } - payRateTV!!.text = String.format("%.2f", payrate) - totalPayTV!!.text = totalPaid - } - } - - override fun onLoaderReset(loader: Loader) {} - - companion object { - private val DEFAULT_LOADER: Int = 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt deleted file mode 100644 index a0cbd9c..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.Manifest -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.Menu -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar -import androidx.core.app.ActivityCompat -import androidx.fragment.app.FragmentManager - -class MainActivity : AppCompatActivity() { - var filter: SharedPreferences? = null - var context: Context? = null - var sortOrder: String? = null - var selection: String? = null - var args: Array? = null - private var toolbar: Toolbar? = null - var fragmentManager: FragmentManager? = null - private var currentFragment: String? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.main_view) - verifyStoragePermissions(this) - toolbar = findViewById(R.id.toolbar) as Toolbar - setSupportActionBar(toolbar) - fragmentManager = supportFragmentManager - val fragmentTransaction = fragmentManager?.beginTransaction() - fragmentTransaction?.replace(R.id.container, FragmentMain())?.addToBackStack("main")?.commit() - fragmentManager?.addOnBackStackChangedListener { - val f = fragmentManager?.fragments - val frag = f?.get(0) - currentFragment = frag?.javaClass?.simpleName - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onBackPressed() { - when (currentFragment) { - "FragmentMain" -> { - AlertDialog.Builder(this) - .setTitle("Leave?") - .setMessage("Are you sure you want to exit Farmr?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> - val intent = Intent(Intent.ACTION_MAIN) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - intent.addCategory(Intent.CATEGORY_HOME) - startActivity(intent) - finish() - System.exit(0) - }.create().show() - return - } - - "FragmentAddItem" -> { - if (FragmentAddItem.Companion.mRadioGroup!!.checkedRadioButtonId == -1) { - fragmentManager!!.popBackStack() - } else { - AlertDialog.Builder(this) - .setTitle("Discard Changes?") - .setMessage("Are you sure you want to discard changes?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> fragmentManager!!.popBackStack() }.create().show() - } - return - } - - else -> if (fragmentManager!!.backStackEntryCount > 1) { - fragmentManager!!.popBackStack() - } - } - } - - fun setActionBarTitle(title: String?) { - toolbar!!.title = title - } - - // Storage Permissions - private val REQUEST_EXTERNAL_STORAGE = 1 - private val PERMISSIONS_STORAGE = arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - - /** - * Checks if the app has permission to write to device storage - * - * If the app does not has permission then the user will be prompted to grant permissions - * - * @param activity - */ - fun verifyStoragePermissions(activity: Activity?) { - // Check if we have write permission - val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - // We don't have permission so prompt the user - ActivityCompat.requestPermissions( - activity, - PERMISSIONS_STORAGE, - REQUEST_EXTERNAL_STORAGE - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt deleted file mode 100644 index 0163f95..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.AlertDialog -import android.content.ContentUris -import android.content.Context -import android.content.DialogInterface -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.OnLongClickListener -import android.view.ViewGroup -import android.widget.CursorAdapter -import android.widget.ImageView -import android.widget.TextView -import androidx.fragment.app.FragmentTransaction -import com.appttude.h_mal.farmr.data.ShiftProvider -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import kotlin.math.floor - -/** - * Created by h_mal on 26/12/2017. - */ -class ShiftsCursorAdapter constructor(private val activity: MainActivity, c: Cursor?) : CursorAdapter(activity, c, 0) { - private var mContext: Context? = null - var shiftProvider: ShiftProvider? = null - override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { - return LayoutInflater.from(context).inflate(R.layout.list_item_1, parent, false) - } - - override fun bindView(view: View, context: Context, cursor: Cursor) { - mContext = context - val descriptionTextView: TextView = view.findViewById(R.id.location) as TextView - val dateTextView: TextView = view.findViewById(R.id.date) as TextView - val totalPay: TextView = view.findViewById(R.id.total_pay) as TextView - val hoursView: TextView = view.findViewById(R.id.hours) as TextView - val h: TextView = view.findViewById(R.id.h) as TextView - val minutesView: TextView = view.findViewById(R.id.minutes) as TextView - val m: TextView = view.findViewById(R.id.m) as TextView - val editView: ImageView = view.findViewById(R.id.imageView) as ImageView - h.text = "h" - m.text = "m" - val typeColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val descriptionColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) - val dateColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) - val durationColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val unitsColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val totalpayColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - descriptionTextView.text = descriptionColumnIndex - dateTextView.text = newDate(dateColumnIndex) - totalPay.text = String.format("%.2f", totalpayColumnIndex) - if ((typeColumnIndex == "Piece Rate") && durationColumnIndex == 0f) { - hoursView.text = unitsColumnIndex.toString() - h.text = "" - minutesView.text = "" - m.text = "pcs" - } else // if(typeColumnIndex.equals("Hourly") || typeColumnIndex.equals("hourly")) - { - hoursView.text = timeValues(durationColumnIndex).get(0) - minutesView.text = timeValues(durationColumnIndex).get(1) - } - val ID: Long = cursor.getLong(cursor.getColumnIndexOrThrow(ShiftsEntry._ID)) - val currentProductUri: Uri = ContentUris.withAppendedId(ShiftsEntry.CONTENT_URI, ID) - val b: Bundle = Bundle() - b.putString("uri", currentProductUri.toString()) - view.setOnClickListener { // activity.clickOnViewItem(ID); - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment2: FurtherInfoFragment = FurtherInfoFragment() - fragment2.arguments = b - fragmentTransaction.replace(R.id.container, fragment2).addToBackStack("furtherinfo").commit() - } - editView.setOnClickListener { - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment3: FragmentAddItem = FragmentAddItem() - fragment3.arguments = b - fragmentTransaction.replace(R.id.container, fragment3).addToBackStack("additem").commit() - } - view.setOnLongClickListener { - println("long click: $ID") - val builder: AlertDialog.Builder = AlertDialog.Builder(mContext) - builder.setMessage("Are you sure you want to delete") - builder.setPositiveButton("delete") { dialog, id -> deleteProduct(ID) } - builder.setNegativeButton("cancel") { dialog, id -> - dialog?.dismiss() - } - val alertDialog: AlertDialog = builder.create() - alertDialog.show() - true - } - } - - private fun deleteProduct(id: Long) { - val args: Array = arrayOf(id.toString()) - // String whereClause = String.format(ShiftsEntry._ID + " in (%s)", new Object[] { TextUtils.join(",", Collections.nCopies(args.length, "?")) }); //for deleting multiple lines - mContext!!.contentResolver.delete(ShiftsEntry.CONTENT_URI, ShiftsEntry._ID + "=?", args) - } - - private fun newDate(dateString: String): String { - var returnString: String? = "01/01/2010" - val year: String = dateString.substring(0, 4) - val month: String = dateString.substring(5, 7) - val day: String = dateString.substring(8) - returnString = "$day-$month-$year" - return returnString - } - - companion object { - fun timeValues(duration: Float): Array { - val hours: Int = floor(duration.toDouble()).toInt() - val minutes: Int = ((duration - hours) * 60).toInt() - val hoursString: String = hours.toString() + "" - val minutesString: String = String.format("%02d", minutes) - return arrayOf(hoursString, minutesString) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt new file mode 100644 index 0000000..3ade301 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.base + +interface BackPressedListener { + fun onBackPressed(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt new file mode 100644 index 0000000..16a8384 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt @@ -0,0 +1,58 @@ +package com.appttude.h_mal.farmr.base + +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelLazy +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.getGenericClassAt +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein +import org.kodein.di.generic.instance + +abstract class BaseActivity : AppCompatActivity(), KodeinAware { + + override val kodein by kodein() + private val factory by instance() + + val viewModel: V by getViewModel() + + private fun getViewModel(): Lazy = + ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, + factoryProducer = { factory } ) + + + /** + * Creates a loading view which to be shown during async operations + * + * #setOnClickListener(null) is an ugly work around to prevent under being clicked during + * loading + */ + + fun startActivity(activity: Class) { + val intent = Intent(this, activity) + startActivity(intent) + } + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onStarted() {} + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onSuccess(data: Any?) {} + + /** + * Called in case of failure or some error emitted from the liveData in viewModel + */ + open fun onFailure(error: Any?) { + if (error is String) displayToast(error) + } + + fun setTitleInActionBar(title: String) { + setTitle(title) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt new file mode 100644 index 0000000..affb4b0 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -0,0 +1,79 @@ +package com.appttude.h_mal.farmr.base + +import android.os.Bundle +import android.view.View +import androidx.annotation.LayoutRes +import androidx.fragment.app.Fragment +import androidx.fragment.app.createViewModelLazy +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getGenericClassAt +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.kodein +import org.kodein.di.generic.instance +import kotlin.properties.Delegates + +@Suppress("EmptyMethod", "EmptyMethod") +abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : + Fragment(contentLayoutId), KodeinAware { + + override val kodein by kodein() + private val factory by instance() + + val viewModel: V by getActivityViewModel() + + private fun getActivityViewModel() = createViewModelLazy( + getGenericClassAt(0), + { requireActivity().viewModelStore }, + { factory }) + + var mActivity: BaseActivity<*>? = null + + private var shortAnimationDuration by Delegates.notNull() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mActivity = requireActivity() as BaseActivity<*> + configureObserver() + } + + private fun configureObserver() { + viewModel.uiState.observe(viewLifecycleOwner) { + when (it) { + is ViewState.HasStarted -> onStarted() + is ViewState.HasData<*> -> onSuccess(it.data) + is ViewState.HasError<*> -> onFailure(it.error) + } + } + } + + /** + * Called in case of starting operation liveData in viewModel + */ + open fun onStarted() { + mActivity?.onStarted() + } + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onSuccess(data: Any?) { + mActivity?.onSuccess(data) + } + + /** + * Called in case of failure or some error emitted from the liveData in viewModel + */ + open fun onFailure(error: Any?) { + mActivity?.onFailure(error) + } + + fun setTitle(title: String) { + (requireActivity() as BaseActivity<*>).setTitleInActionBar(title) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt new file mode 100644 index 0000000..ab62a76 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt @@ -0,0 +1,47 @@ +package com.appttude.h_mal.farmr.base + +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.appttude.h_mal.farmr.utils.generateView + +open class BaseRecyclerAdapter( + @LayoutRes private val emptyViewId: Int, + @LayoutRes private val currentViewId: Int +): RecyclerView.Adapter() { + var list: List? = null + + fun updateData(newList: List) { + list = newList + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return if (list.isNullOrEmpty()) { + val emptyViewHolder = parent.generateView(emptyViewId) + EmptyViewHolder(emptyViewHolder) + } else { + val currentViewHolder = parent.generateView(currentViewId) + CurrentViewHolder(currentViewHolder) + } + } + + override fun getItemCount(): Int { + return if (list.isNullOrEmpty()) 1 else list!!.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + when (holder) { + is EmptyViewHolder -> bindEmptyView(holder.itemView) + is CurrentViewHolder -> bindCurrentView(holder.itemView, position, list!![position]) + } + } + + open fun bindEmptyView(view: View) {} + open fun bindCurrentView(view: View, position: Int, data: T) {} + + class EmptyViewHolder(itemView: View): ViewHolder(itemView) + class CurrentViewHolder(itemView: View): ViewHolder(itemView) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt new file mode 100644 index 0000000..40c0b5c --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt @@ -0,0 +1,26 @@ +package com.appttude.h_mal.farmr.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.appttude.h_mal.farmr.model.ViewState +import java.lang.Exception + +open class BaseViewModel: ViewModel() { + + private val _uiState = MutableLiveData() + val uiState: LiveData = _uiState + + + fun onStart() { + _uiState.postValue(ViewState.HasStarted) + } + + fun onSuccess(result: T) { + _uiState.postValue(ViewState.HasData(result)) + } + + protected fun onError(error: E) { + _uiState.postValue(ViewState.HasError(error)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt new file mode 100644 index 0000000..db97694 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt @@ -0,0 +1,24 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.Sortable + +interface Repository { + fun insertShiftIntoDatabase(shift: Shift): Boolean + fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean + fun readShiftsFromDatabase(): List? + fun readSingleShiftFromDatabase(id: Long): ShiftObject? + fun deleteSingleShiftFromDatabase(id: Long): Boolean + fun deleteAllShiftsFromDatabase(): Boolean + fun retrieveSortAndOrderFromPref(): Pair + fun setSortAndOrderFromPref(sortable: Sortable, order: Order) + fun retrieveFilteringDetailsInPrefs(): Map + fun setFilteringDetailsInPrefs( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt new file mode 100644 index 0000000..3af7652 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt @@ -0,0 +1,71 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.Sortable + +class RepositoryImpl( + private val legacyDatabase: LegacyDatabase, + private val preferenceProvider: PreferenceProvider +): Repository { + override fun insertShiftIntoDatabase(shift: Shift): Boolean { + return legacyDatabase.insertShiftDataIntoDatabase(shift) != null + } + + override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean { + return legacyDatabase.updateShiftDataIntoDatabase( + id = id, + typeString = shift.type.type, + descriptionString = shift.description, + dateString = shift.date, + timeInString = shift.timeIn ?: "", + timeOutString = shift.timeOut ?: "", + duration = shift.duration ?: 0f, + breaks = shift.breakMins ?: 0, + units = shift.units ?: 0f, + payRate = shift.rateOfPay, + totalPay = shift.totalPay + ) == 1 + } + + override fun readShiftsFromDatabase(): List? { + return legacyDatabase.readShiftsFromDatabase() + } + + override fun readSingleShiftFromDatabase(id: Long): ShiftObject? { + return legacyDatabase.readSingleShiftWithId(id) + } + + override fun deleteSingleShiftFromDatabase(id: Long): Boolean { + return legacyDatabase.deleteSingleShift(id) == 1 + } + + override fun deleteAllShiftsFromDatabase(): Boolean { + return legacyDatabase.deleteAllShiftsInDatabase() > 0 + } + + override fun retrieveSortAndOrderFromPref(): Pair { + return preferenceProvider.getSortableAndOrder() + } + + override fun setSortAndOrderFromPref(sortable: Sortable, order: Order) { + preferenceProvider.saveSortableAndOrder(sortable, order) + } + + override fun retrieveFilteringDetailsInPrefs(): Map { + return preferenceProvider.getFilteringDetails() + } + + override fun setFilteringDetailsInPrefs( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) { + preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt new file mode 100644 index 0000000..c413f9f --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt @@ -0,0 +1,167 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType + +class LegacyDatabase(context: Context) { + private val resolver = context.contentResolver + + private val projection = arrayOf( + _ID, + COLUMN_SHIFT_DESCRIPTION, + COLUMN_SHIFT_DATE, + COLUMN_SHIFT_TIME_IN, + COLUMN_SHIFT_TIME_OUT, + COLUMN_SHIFT_BREAK, + COLUMN_SHIFT_DURATION, + COLUMN_SHIFT_TYPE, + COLUMN_SHIFT_UNIT, + COLUMN_SHIFT_PAYRATE, + COLUMN_SHIFT_TOTALPAY + ) + + // Create + fun insertShiftDataIntoDatabase( + shift: Shift + ): Uri? { + val values = ContentValues().apply { + put(COLUMN_SHIFT_TYPE, shift.type.type) + put(COLUMN_SHIFT_DESCRIPTION, shift.description) + put(COLUMN_SHIFT_DATE, shift.description) + put(COLUMN_SHIFT_TIME_IN, shift.timeIn ?: "00:00") + put(COLUMN_SHIFT_TIME_OUT, shift.timeOut ?: "00:00") + put(COLUMN_SHIFT_DURATION, shift.duration ?: 0.00f) + put(COLUMN_SHIFT_BREAK, shift.breakMins ?: 0) + put(COLUMN_SHIFT_UNIT, shift.units ?: 0.00f) + put(COLUMN_SHIFT_PAYRATE, shift.rateOfPay) + put(COLUMN_SHIFT_TOTALPAY, shift.totalPay) + } + return resolver.insert(CONTENT_URI, values) + } + + // Read + fun readShiftsFromDatabase(): List? { + val cursor = resolver.query( + CONTENT_URI, + projection, + null, null, null + ) ?: return null + cursor.moveToFirst() + val shifts = (0..cursor.count).map { cursor.getShift() } + // close cursor after query operations + cursor.close() + + return shifts + } + + fun readSingleShiftWithId(id: Long): ShiftObject? { + val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id) + + val cursor = resolver.query( + itemUri, + projection, + null, null, null + ) ?: return null + cursor.moveToFirst() + + val shift = cursor.takeIf { it.moveToFirst() }?.run { getShift() } ?: return null + cursor.close() + return shift + } + + // Update + fun updateShiftDataIntoDatabase( + id: Long, + typeString: String, + descriptionString: String, + dateString: String, + timeInString: String, + timeOutString: String, + duration: Float, + breaks: Int, + units: Float, + payRate: Float, + totalPay: Float, + ): Int { + val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id) + + val values = ContentValues().apply { + put(COLUMN_SHIFT_TYPE, typeString) + put(COLUMN_SHIFT_DESCRIPTION, descriptionString) + put(COLUMN_SHIFT_DATE, dateString) + put(COLUMN_SHIFT_TIME_IN, timeInString) + put(COLUMN_SHIFT_TIME_OUT, timeOutString) + put(COLUMN_SHIFT_DURATION, duration) + put(COLUMN_SHIFT_BREAK, breaks) + put(COLUMN_SHIFT_UNIT, units) + put(COLUMN_SHIFT_PAYRATE, payRate) + put(COLUMN_SHIFT_TOTALPAY, totalPay) + } + return resolver.update(itemUri, values, null, null) + } + + // Delete + fun deleteAllShiftsInDatabase(): Int { + return resolver.delete(CONTENT_URI, null, null) + } + + fun deleteSingleShift(id: Long): Int { + val args: Array = arrayOf(id.toString()) + return resolver.delete(CONTENT_URI, "$_ID=?", args) + } + + private fun Cursor.getShift(): ShiftObject = run { + val id = getLong(getColumnIndexOrThrow(_ID)) + val descriptionColumnIndex = getString( + getColumnIndexOrThrow( + COLUMN_SHIFT_DESCRIPTION + ) + ) + val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE)) + val timeInColumnIndex = + getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN)) + val timeOutColumnIndex = + getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_OUT)) + val durationColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_DURATION)) + val breakOutColumnIndex = + getInt(getColumnIndexOrThrow(COLUMN_SHIFT_BREAK)) + val typeColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TYPE)) + val unitColumnIndex = getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_UNIT)) + val payrateColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_PAYRATE)) + val totalpayColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_TOTALPAY)) + + ShiftObject( + id = id, + type = typeColumnIndex, + description = descriptionColumnIndex, + date = dateColumnIndex, + timeIn = timeInColumnIndex, + timeOut = timeOutColumnIndex, + duration = durationColumnIndex, + breakMins = breakOutColumnIndex, + units = unitColumnIndex, + rateOfPay = payrateColumnIndex, + totalPay = totalpayColumnIndex + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt new file mode 100644 index 0000000..af40553 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt @@ -0,0 +1,28 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import kotlin.math.floor + +data class ShiftObject( + val id: Long, + val type: String, + val description: String, + val date: String, + val timeIn: String, + val timeOut: String, + val duration: Float, + val breakMins: Int, + val units: Float, + val rateOfPay: Float, + val totalPay: Float +) { + fun copyToShift() = Shift(ShiftType.getEnumByType(type), description, date, timeIn, timeOut, duration, breakMins, units, rateOfPay, totalPay) + + fun getHoursMinutesPairFromDuration(): Pair { + val hours: Int = floor(duration).toInt() + val minutes: Int = ((duration - hours) * 60).toInt() + return Pair(hours.toString(), minutes.toString()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt similarity index 97% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt index d6cc534..87ad3d8 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt @@ -1,15 +1,13 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentProvider import android.content.ContentUris import android.content.ContentValues -import android.content.Context import android.content.UriMatcher import android.database.Cursor import android.net.Uri import android.util.Log -import androidx.annotation.VisibleForTesting -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry /** * Created by h_mal on 26/12/2017. diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt similarity index 96% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt index ac1a5d0..9fa0a96 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.net.Uri diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt similarity index 95% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt index fe83ed6..4918518 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt @@ -1,9 +1,9 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry /** * Created by h_mal on 26/12/2017. diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt new file mode 100644 index 0000000..99004a6 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -0,0 +1,65 @@ +package com.appttude.h_mal.farmr.data.prefs + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable + +/** + * Shared preferences to save & load last timestamp + */ +const val SORT = "SORT" +const val ORDER = "ORDER" + +const val DESCRIPTION = "DESCRIPTION" +const val TIME_IN = "TIME_IN" +const val TIME_OUT = "TIME_OUT" +const val TYPE = "TYPE" + +class PreferenceProvider( + context: Context +) { + + private val appContext = context.applicationContext + + private val preference: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(appContext) + + fun saveSortableAndOrder(sortable: Sortable, order: Order) { + preference.edit() + .putString(SORT, sortable.label) + .putString(ORDER, order.label) + .apply() + } + + fun getSortableAndOrder(): Pair { + val sort = preference.getString(SORT, null)?.let { Sortable.valueOf(it) } + val order = preference.getString(ORDER, null)?.let { Order.valueOf(it) } + + return Pair(sort, order) + } + fun saveFilteringDetails( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) { + preference.edit() + .putString(DESCRIPTION, description) + .putString(TIME_IN, timeIn) + .putString(TIME_OUT, timeOut) + .putString(TYPE, type) + .apply() + } + + fun getFilteringDetails(): Map { + return mapOf( + Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)), + Pair(TIME_IN, preference.getString(TIME_IN, null)), + Pair(TIME_OUT, preference.getString(TIME_OUT, null)), + Pair(TYPE, preference.getString(TYPE, null)) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt new file mode 100644 index 0000000..ced4fb4 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -0,0 +1,28 @@ +package com.appttude.h_mal.farmr.di + +import android.app.Application +import com.appttude.h_mal.farmr.data.RepositoryImpl +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +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 ShiftApplication: Application(), KodeinAware { + // Kodein creation of modules to be retrieve within the app + override val kodein = Kodein.lazy { + import(androidXModule(this@ShiftApplication)) + + bind() from singleton { LegacyDatabase(this@ShiftApplication) } + bind() from singleton { PreferenceProvider(this@ShiftApplication) } + bind() from singleton { RepositoryImpl(instance(), instance()) } + + bind() from provider { ApplicationViewModelFactory(instance()) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt new file mode 100644 index 0000000..4120cdc --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt @@ -0,0 +1,11 @@ +package com.appttude.h_mal.farmr.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class EntityItem( + @PrimaryKey(autoGenerate = false) + val id: String, + val shift: Shift +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt new file mode 100644 index 0000000..45a3e87 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt @@ -0,0 +1,8 @@ +package com.appttude.h_mal.farmr.model + +data class FilterStore( + val description: String?, + val dateFrom: String?, + val dateTo: String?, + val type: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt new file mode 100644 index 0000000..b7c971f --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.model + +enum class Order(val label: String) { + ASCENDING("Ascending"), DESCENDING("Descending") +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 5d76543..3d8e5ab 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -1,14 +1,68 @@ package com.appttude.h_mal.farmr.model +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.convertTimeStringToHourMinutesPair +import com.appttude.h_mal.farmr.utils.formatToTwoDp +import java.io.IOException + data class Shift( - val type: ShiftType, - val description: String, - val date: String, - val timeIn: String?, - val timeOut: String?, - val duration: Float?, - val breakMins: Int?, - val units: Float?, - val rateOfPay: Float, - val totalPay: Float -) \ No newline at end of file + val type: ShiftType, + val description: String, + val date: String, + val timeIn: String?, + val timeOut: String?, + val duration: Float?, + val breakMins: Int?, + val units: Float?, + val rateOfPay: Float, + val totalPay: Float +) { + companion object { + // Invocation for Hourly + operator fun invoke( + description: String, + date: String, + timeIn: String, + timeOut: String, + breakMins: Int? = null, + rateOfPay: Float + ): Shift { + val breakTime = breakMins ?: 0 + val duration = calculateDuration(timeIn, timeOut, breakTime) + + return Shift( + ShiftType.HOURLY, + description, + date, + timeIn, + timeOut, + duration, + breakTime, + duration, + rateOfPay, + (duration * rateOfPay).formatToTwoDp() + ) + } + + operator fun invoke( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) = Shift( + ShiftType.PIECE, + description, + date, + "", + "", + 0f, + 0, + units, + rateOfPay, + (units * rateOfPay).formatToTwoDp() + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt index d4d1118..b724c19 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt @@ -1,6 +1,16 @@ package com.appttude.h_mal.farmr.model -enum class ShiftType(val type: String) { +enum class ShiftType(val type: String){ HOURLY("Hourly"), - PIECE("Piece Rate") + PIECE("Piece Rate"); + + fun getEnumByType(type: String): ShiftType { + return values().first { it.type == type } + } + + companion object { + fun getEnumByType(type: String): ShiftType { + return values().first { it.type == type } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt new file mode 100644 index 0000000..6a0c9a1 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -0,0 +1,11 @@ +package com.appttude.h_mal.farmr.model + +enum class Sortable(val label: String) { + ID("Default"), + TYPE("Shift Type"), + DATE("Date"), + DESCRIPTION("Description"), + DURATION("Added"), UNITS("Duration"), + RATEOFPAY("Rate of pay"), + TOTALPAY("Total Pay") +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt new file mode 100644 index 0000000..2080f55 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt @@ -0,0 +1,7 @@ +package com.appttude.h_mal.farmr.model + +sealed class ViewState { + object HasStarted : ViewState() + class HasData(val data: T) : ViewState() + class HasError(val error: T) : ViewState() +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt new file mode 100644 index 0000000..ca917f3 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -0,0 +1,92 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.view.View.OnClickListener +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import androidx.core.widget.doAfterTextChanged +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.setDatePicker +import com.appttude.h_mal.farmr.viewmodel.MainViewModel + +class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), + AdapterView.OnItemSelectedListener, OnClickListener { + private val spinnerList: Array = + arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type) + + private lateinit var LocationET: EditText + private lateinit var dateFromET: EditText + private lateinit var dateToET: EditText + private lateinit var typeSpinner: Spinner + + private var description: String? = null + private var dateFrom: String? = null + private var dateTo: String? = null + private var type: String? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setTitle(getString(R.string.title_activity_filter_data)) + + LocationET = view.findViewById(R.id.filterLocationEditText) + dateFromET = view.findViewById(R.id.fromdateInEditText) + dateToET = view.findViewById(R.id.filterDateOutEditText) + typeSpinner = view.findViewById(R.id.TypeFilterEditText) + val submit: Button = view.findViewById(R.id.submitFiltered) + + val adapter: ArrayAdapter = + ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList) + typeSpinner.adapter = adapter + + val filterDetails = viewModel.getFiltrationDetails() + + filterDetails.let { + LocationET.setText(it.description) + dateFromET.setText(it.dateFrom) + dateToET.setText(it.dateTo) + + it.type?.let { t -> + val spinnerPosition: Int = adapter.getPosition(t) + typeSpinner.setSelection(spinnerPosition) + } + + } + + LocationET.doAfterTextChanged { description = it.toString() } + dateFromET.setDatePicker { dateFrom = it } + dateToET.setDatePicker { dateTo = it } + typeSpinner.onItemSelectedListener = this + + submit.setOnClickListener(this) + } + + override fun onItemSelected( + parentView: AdapterView<*>?, + selectedItemView: View?, + position: Int, + id: Long + ) { + type = when (position) { + 1 -> ShiftType.HOURLY.toString() + 2 -> ShiftType.PIECE.toString() + else -> return + } + } + + override fun onNothingSelected(parentView: AdapterView<*>?) {} + + private fun submitFiltrationDetails() { + viewModel.setFiltrationDetails(description, dateFrom, dateTo, type) + } + + override fun onClick(p0: View?) { + submitFiltrationDetails() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt new file mode 100644 index 0000000..f201bf1 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -0,0 +1,229 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.ScrollView +import android.widget.TextView +import androidx.core.widget.doAfterTextChanged +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.formatToTwoDpString +import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.popBackStack +import com.appttude.h_mal.farmr.utils.setDatePicker +import com.appttude.h_mal.farmr.utils.setTimePicker +import com.appttude.h_mal.farmr.utils.show +import com.appttude.h_mal.farmr.utils.validateField +import com.appttude.h_mal.farmr.viewmodel.MainViewModel + +class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), + RadioGroup.OnCheckedChangeListener, BackPressedListener { + + private lateinit var mHourlyRadioButton: RadioButton + private lateinit var mPieceRadioButton: RadioButton + private lateinit var mLocationEditText: EditText + private lateinit var mDateEditText: EditText + private lateinit var mDurationTextView: TextView + private lateinit var mTimeInEditText: EditText + private lateinit var mTimeOutEditText: EditText + private lateinit var mBreakEditText: EditText + private lateinit var mUnitEditText: EditText + private lateinit var mPayRateEditText: EditText + private lateinit var mTotalPayTextView: TextView + private lateinit var hourlyDataView: LinearLayout + private lateinit var unitsHolder: LinearLayout + private lateinit var durationHolder: LinearLayout + private lateinit var wholeView: LinearLayout + private lateinit var scrollView: ScrollView + private lateinit var submitProduct: Button + private lateinit var mRadioGroup: RadioGroup + + private var mDate: String? = null + private var mDescription: String? = null + private var mTimeIn: String? = null + private var mTimeOut: String? = null + private var mBreaks = 0 + private var mUnits = 0f + private var mPayRate = 0f + private var mType: ShiftType? = null + private var mDuration: Float = 0f + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + scrollView = view.findViewById(R.id.total_view) + mRadioGroup = view.findViewById(R.id.rg) + mHourlyRadioButton = view.findViewById(R.id.hourly) + mPieceRadioButton = view.findViewById(R.id.piecerate) + mLocationEditText = view.findViewById(R.id.locationEditText) + mDateEditText = view.findViewById(R.id.dateEditText) + mTimeInEditText = view.findViewById(R.id.timeInEditText) + mBreakEditText = view.findViewById(R.id.breakEditText) + mTimeOutEditText = view.findViewById(R.id.timeOutEditText) + mDurationTextView = view.findViewById(R.id.ShiftDuration) + mUnitEditText = view.findViewById(R.id.unitET) + mPayRateEditText = view.findViewById(R.id.payrateET) + mTotalPayTextView = view.findViewById(R.id.totalpayval) + hourlyDataView = view.findViewById(R.id.hourly_data_holder) + unitsHolder = view.findViewById(R.id.units_holder) + durationHolder = view.findViewById(R.id.duration_holder) + wholeView = view.findViewById(R.id.whole_view) + submitProduct = view.findViewById(R.id.submit) + + mRadioGroup.setOnCheckedChangeListener(this) + mLocationEditText.doAfterTextChanged { + mDescription = it.toString() + } + mDateEditText.setDatePicker { mDate = it } + mTimeInEditText.setTimePicker { + mTimeIn = it + calculateTotalPay() + } + mTimeOutEditText.setTimePicker { + mTimeOut = it + calculateTotalPay() + } + mBreakEditText.doAfterTextChanged { + mBreaks = it.toString().toIntOrNull() ?: 0 + calculateTotalPay() + } + mUnitEditText.doAfterTextChanged { + it.toString().toFloatOrNull()?.let { u -> mPayRate = u } + calculateTotalPay() + } + mPayRateEditText.doAfterTextChanged { + it.toString().toFloatOrNull()?.let { p -> + mPayRate = p + calculateTotalPay() + } + } + + submitProduct.setOnClickListener { submitShift() } + + setupViewAfterViewCreated() + } + + private fun setupViewAfterViewCreated() { + val title = when (arguments?.containsKey(ID)) { + true -> { + // Since we are editing a shift lets load the shift data into the views + viewModel.getCurrentShift(arguments!!.getLong(ID))?.run { + mLocationEditText.setText(description) + mDateEditText.setText(date) + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + mHourlyRadioButton.isChecked = true + mPieceRadioButton.isChecked = false + mTimeInEditText.setText(timeIn) + mTimeOutEditText.setText(timeOut) + mBreakEditText.setText(breakMins.toString()) + val durationText = "${duration.formatToTwoDpString()} Hours" + mDurationTextView.text = durationText + } + + ShiftType.PIECE -> { + mHourlyRadioButton.isChecked = false + mPieceRadioButton.isChecked = true + mUnitEditText.setText(units.formatToTwoDpString()) + } + } + mPayRateEditText.setText(rateOfPay.formatToTwoDpString()) + mTotalPayTextView.text = totalPay.formatToTwoDpString() + } + + // Return title + getString(R.string.edit_item_title) + } + + else -> getString(R.string.add_item_title) + } + setTitle(title) + } + + override fun onCheckedChanged(radioGroup: RadioGroup, id: Int) { + when (radioGroup.checkedRadioButtonId) { + R.id.hourly -> { + mType = ShiftType.HOURLY + wholeView.show() + unitsHolder.hide() + hourlyDataView.show() + durationHolder.show() + } + R.id.piecerate -> { + mType = ShiftType.PIECE + wholeView.show() + unitsHolder.show() + hourlyDataView.hide() + durationHolder.hide() + } + } + } + + private fun submitShift() { + mDate.validateField({ !it.isNullOrBlank() }){ + onFailure("Date field cannot be empty") + return + } + mDescription.validateField({ !it.isNullOrBlank() }){ + onFailure("Description field cannot be empty") + return + } + mPayRate.validateField({ !it.isNaN() }){ + onFailure("Rate of pay field cannot be empty") + return + } + + if (mPieceRadioButton.isChecked) { + mUnits.validateField({!it.isNaN()}) { + onFailure("Units field cannot be empty") + return + } + viewModel.insertPieceRateShift(mDescription!!, mDate!!, mUnits, mPayRate) + } else if (mHourlyRadioButton.isChecked) { + viewModel.insertHourlyShift(mDescription!!, mDate!!, mPayRate, mTimeIn, mTimeOut, mBreaks) + } + } + + private fun calculateTotalPay() { + mType?.let { + val total = when (it) { + ShiftType.HOURLY -> { + // Calculate duration before total pay calculation + mDuration = viewModel.retrieveDurationText(mTimeIn, mTimeOut, mBreaks) ?: return + mDurationTextView.text = StringBuilder().append(mDuration).append(" hours").toString() + mDuration * mPayRate + } + ShiftType.PIECE -> { + mUnits * mPayRate + } + } + mTotalPayTextView.text = total.formatToTwoDpString() + } + } + + override fun onBackPressed(): Boolean { + if (mRadioGroup.checkedRadioButtonId == -1) { + mActivity?.popBackStack() + } else { + requireContext().createDialog( + title = "Discard Changes?", + message = "Are you sure you want to discard changes?", + displayCancel = true, + okCallback = { _, _ -> + mActivity?.popBackStack() + } + ) + } + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt new file mode 100644 index 0000000..6d053d6 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -0,0 +1,367 @@ +package com.appttude.h_mal.farmr.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.recyclerview.widget.RecyclerView +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.google.android.material.floatingactionbutton.FloatingActionButton +import kotlin.system.exitProcess + +class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { + lateinit var activity: MainActivity + private lateinit var productListView: RecyclerView + private lateinit var mAdapter: ShiftRecyclerAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Inflate the layout for this fragment + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mAdapter = ShiftRecyclerAdapter(this) { + viewModel.deleteShift(it) + } + productListView = view.findViewById(R.id.list_item_view) + productListView.adapter = mAdapter + + view.findViewById(R.id.fab1).setOnClickListener { + navigateToFragment(FragmentAddItem(), name = "additem") + } + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is List<*>) { + mAdapter.updateData(data as List) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.delete_all -> { + deleteAllProducts() + return true + } + + R.id.help -> { + AlertDialog.Builder(context) + .setTitle("Help & Support:") + .setView(R.layout.dialog_layout) + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> arg0.dismiss() } + .create().show() + return true + } + + R.id.filter_data -> { +// val fragmentTransaction: FragmentTransaction = +// activity.fragmentManager!!.beginTransaction() +// fragmentTransaction.replace(R.id.container, FilterDataFragment()) +// .addToBackStack("filterdata").commit() + // Todo: filter shift + + return true + } + + R.id.sort_data -> { + sortData() + return true + } + R.id.clear_filter -> { + // Todo: Apply filter to list + return true + } + + R.id.export_data -> { + if (checkStoragePermissions(activity)) { + AlertDialog.Builder(context) + .setTitle("Export?") + .setMessage("Exporting current filtered data. Continue?") + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() } + .create().show() + } else { + Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT) + .show() + } + return true + } + + R.id.action_favorite -> { + AlertDialog.Builder(context) + .setTitle("Info:") + .setMessage(viewModel.getInformation()) + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> + arg0.dismiss() + }.create().show() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun sortData() { + val groupName = Sortable.entries.map { it.label }.toTypedArray() + var sort = Sortable.ID + + val sortAndOrder = viewModel.getSortAndOrder() + val checkedItem = Sortable.values().indexOf(sortAndOrder.first) + + AlertDialog.Builder(context) + .setTitle("Sort by:") + .setSingleChoiceItems( + groupName, + checkedItem + ) { p0, p1 -> sort = Sortable.valueOf(groupName[p1]) } + .setPositiveButton("Ascending") { dialog, id -> + viewModel.setSortAndOrder(sort) + dialog.dismiss() + }.setNegativeButton("Descending") { dialog, id -> + viewModel.setSortAndOrder(sort, Order.DESCENDING) + dialog.dismiss() + } + .create().show() + } + + private fun deleteAllProducts() { + requireContext().createDialog( + "Warning", + message = "Are you sure you want to delete all date?", + displayCancel = true, + okCallback = { _, _ -> + viewModel.deleteAllShifts() + } + ) + } + + private fun ExportData() { +// val permission = +// ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) +// if (permission != PackageManager.PERMISSION_GRANTED) { +// Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() +// return +// } +// shiftsDbhelper = ShiftsDbHelper(context) +// val database = shiftsDbhelper!!.writableDatabase +// val projection_export = arrayOf( +// ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, +// ShiftsEntry.COLUMN_SHIFT_DATE, +// ShiftsEntry.COLUMN_SHIFT_TIME_IN, +// ShiftsEntry.COLUMN_SHIFT_TIME_OUT, +// ShiftsEntry.COLUMN_SHIFT_BREAK, +// ShiftsEntry.COLUMN_SHIFT_DURATION, +// ShiftsEntry.COLUMN_SHIFT_TYPE, +// ShiftsEntry.COLUMN_SHIFT_UNIT, +// ShiftsEntry.COLUMN_SHIFT_PAYRATE, +// ShiftsEntry.COLUMN_SHIFT_TOTALPAY +// ) +// val cursor = activity.contentResolver.query( +// ShiftsEntry.CONTENT_URI, +// projection_export, +// activity.selection, +// activity.args, +// activity.sortOrder +// ) +// database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null) +// var totalDuration = 0.00f +// var totalUnits = 0.00f +// var totalPay = 0.00f +// try { +// while (cursor!!.moveToNext()) { +// val descriptionColumnIndex = +// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) +// val dateColumnIndex = +// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) +// val timeInColumnIndex = +// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) +// val timeOutColumnIndex = +// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) +// val durationColumnIndex = +// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) +// val breakOutColumnIndex = +// cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK)) +// val typeColumnIndex = +// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) +// val unitColumnIndex = +// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) +// val payrateColumnIndex = +// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE)) +// val totalpayColumnIndex = +// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) +// totalUnits = totalUnits + unitColumnIndex +// totalDuration = totalDuration + durationColumnIndex +// totalPay = totalPay + totalpayColumnIndex +// val values = ContentValues() +// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex) +// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex) +// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) +// } +// } catch (e: Exception) { +// Log.e("FragmentMain", "ExportData: ", e) +// } finally { +// val values = ContentValues() +// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "") +// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "") +// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "") +// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "") +// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:") +// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration) +// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:") +// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits) +// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:") +// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) +// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) +// cursor!!.close() +// } +// val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp" +// val file = File(savePath) +// if (!file.exists()) { +// file.mkdirs() +// } +// val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath) +// sqLiteToExcel.exportSingleTable( +// "shiftsexport", +// "shifthistory.xls", +// object : ExportListener { +// override fun onStart() {} +// override fun onCompleted(filePath: String) { +// Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show() +// val newPath = Uri.parse("file://$savePath/shifthistory.xls") +// val builder = VmPolicy.Builder() +// StrictMode.setVmPolicy(builder.build()) +// val emailintent = Intent(Intent.ACTION_SEND) +// emailintent.type = "application/vnd.ms-excel" +// emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts") +// emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.") +// emailintent.putExtra(Intent.EXTRA_STREAM, newPath) +// startActivity(Intent.createChooser(emailintent, "Send Email")) +// } +// +// override fun onError(e: Exception) { +// println("Error msg: $e") +// Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show() +// } +// }) + } + + @SuppressLint("DefaultLocale") + fun buildInfoString( + totalDuration: Float, + countOfTypeH: Int, + countOfTypeP: Int, + totalUnits: Float, + totalPay: Float, + lines: Int + ): String { + var textString: String + textString = "$lines Shifts" + if (countOfTypeH != 0 && countOfTypeP != 0) { + textString = "$textString ($countOfTypeH Hourly/$countOfTypeP Piece Rate)" + } + if (countOfTypeH != 0) { + textString = """ + $textString + Total Hours: ${String.format("%.2f", totalDuration)} + """.trimIndent() + } + if (countOfTypeP != 0) { + textString = """ + $textString + Total Units: ${String.format("%.2f", totalUnits)} + """.trimIndent() + } + if (totalPay != 0f) { + textString = """ + $textString + Total Pay: ${"$"}${String.format("%.2f", totalPay)} + """.trimIndent() + } + return textString + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + println("request code$requestCode") + if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { + if (grantResults.size > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + exportDialog() + } else { + Toast.makeText(context, "Storage Permissions denied", Toast.LENGTH_SHORT).show() + } + } + } + + fun exportDialog() { + AlertDialog.Builder(context) + .setTitle("Export?") + .setMessage("Exporting current filtered data. Continue?") + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() + } + + fun checkStoragePermissions(activity: Activity?): Boolean { + var status = false + val permission = ActivityCompat.checkSelfPermission( + activity!!, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + if (permission == PackageManager.PERMISSION_GRANTED) { + status = true + } + return status + } + + companion object { + const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 + } + + override fun onBackPressed(): Boolean { + requireContext().createDialog( + title = "Leave?", + message = "Are you sure you want to exit Farmr?", + displayCancel = true, + okCallback = { _, _ -> + val intent = Intent(Intent.ACTION_MAIN) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.addCategory(Intent.CATEGORY_HOME) + startActivity(intent) + requireActivity().finish() + exitProcess(0) + } + ) + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt new file mode 100644 index 0000000..bda6dfd --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt @@ -0,0 +1,111 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.CURRENCY +import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.utils.show +import com.appttude.h_mal.farmr.viewmodel.MainViewModel + +class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { + private lateinit var typeTV: TextView + private lateinit var descriptionTV: TextView + private lateinit var dateTV: TextView + private lateinit var times: TextView + private lateinit var breakTV: TextView + private lateinit var durationTV: TextView + private lateinit var unitsTV: TextView + private lateinit var payRateTV: TextView + private lateinit var totalPayTV: TextView + private lateinit var hourlyDetailHolder: LinearLayout + private lateinit var unitsHolder: LinearLayout + private lateinit var wholeView: LinearLayout + private lateinit var progressBarFI: ProgressBar + private lateinit var editButton: Button + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTitle(getString(R.string.further_info_title)) + + progressBarFI = view.findViewById(R.id.progressBar_info) + wholeView = view.findViewById(R.id.further_info_view) + typeTV = view.findViewById(R.id.details_shift) + descriptionTV = view.findViewById(R.id.details_desc) + dateTV = view.findViewById(R.id.details_date) + times = view.findViewById(R.id.details_time) + breakTV = view.findViewById(R.id.details_breaks) + durationTV = view.findViewById(R.id.details_duration) + unitsTV = view.findViewById(R.id.details_units) + payRateTV = view.findViewById(R.id.details_pay_rate) + totalPayTV = view.findViewById(R.id.details_totalpay) + editButton = view.findViewById(R.id.details_edit) + hourlyDetailHolder = view.findViewById(R.id.details_hourly_details) + unitsHolder = view.findViewById(R.id.details_units_holder) + + val id = arguments!!.getLong(ID) + + editButton.setOnClickListener { + navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!) + } + + setupView(id) + } + + private fun setupView(id: Long) { + viewModel.getCurrentShift(id)?.run { + typeTV.text = type + descriptionTV.text = description + dateTV.text = date + payRateTV.text = rateOfPay.toString() + totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() + + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + hourlyDetailHolder.show() + unitsHolder.hide() + times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() + breakTV.text = StringBuilder(breakMins).append("mins").toString() + durationTV.text = buildDurationSummary(this) + val paymentSummary = + StringBuilder().append(duration).append(" Hours @ ").append(CURRENCY) + .append(rateOfPay).append(" per Hour").append("\n") + .append("Equals: ").append(CURRENCY).append(totalPay) + totalPayTV.text = paymentSummary + } + + ShiftType.PIECE -> { + hourlyDetailHolder.hide() + unitsHolder.show() + unitsTV.text = units.toString() + + val paymentSummary = + StringBuilder().append(units).append(" Units @ ").append(CURRENCY) + .append(rateOfPay).append(" per Unit").append("\n") + .append("Equals: ").append(CURRENCY).append(totalPay) + totalPayTV.text = paymentSummary + } + } + } + } + + private fun buildDurationSummary(shiftObject: ShiftObject): String { + val time = shiftObject.getHoursMinutesPairFromDuration() + + val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) + .append(" Minutes ") + if (shiftObject.breakMins > 0) { + stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") + } + return stringBuilder.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt new file mode 100644 index 0000000..87bb453 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -0,0 +1,94 @@ +package com.appttude.h_mal.farmr.ui + +import android.Manifest +import android.R.string.cancel +import android.R.string.ok +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.Menu +import androidx.appcompat.widget.Toolbar +import androidx.core.app.ActivityCompat +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseActivity +import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.popBackStack +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import kotlin.system.exitProcess + +class MainActivity : BaseActivity() { + private lateinit var toolbar: Toolbar + + var selection: String? = null + var args: Array? = null + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.main_view) + toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + + verifyStoragePermissions(this) + + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.container, FragmentMain()).addToBackStack("main").commit() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onBackPressed() { + val currentFragment = supportFragmentManager.findFragmentById(R.id.container) + if (currentFragment is BackPressedListener) { + currentFragment.onBackPressed() + } else { + if (supportFragmentManager.backStackEntryCount > 1) { + popBackStack() + } else { + super.onBackPressed() + } + } + } + + + fun setActionBarTitle(title: String?) { + toolbar.title = title + } + + // Storage Permissions + private val REQUEST_EXTERNAL_STORAGE = 1 + private val PERMISSIONS_STORAGE = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + + /** + * Checks if the app has permission to write to device storage + * + * If the app does not has permission then the user will be prompted to grant permissions + * + * @param activity + */ + fun verifyStoragePermissions(activity: Activity?) { + // Check if we have write permission + val permission = ActivityCompat.checkSelfPermission( + activity!!, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + if (permission != PackageManager.PERMISSION_GRANTED) { + // We don't have permission so prompt the user + ActivityCompat.requestPermissions( + activity, + PERMISSIONS_STORAGE, + REQUEST_EXTERNAL_STORAGE + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt new file mode 100644 index 0000000..acb99da --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt @@ -0,0 +1,104 @@ +package com.appttude.h_mal.farmr.ui + +import android.app.AlertDialog +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.RecyclerView +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.navigateToFragment + +class ShiftRecyclerAdapter( + private val fragment: Fragment, + private val longPressCallback: (Long) -> Unit +) : BaseRecyclerAdapter( + emptyViewId = R.layout.empty_list_view, + currentViewId = R.layout.list_item_1 +) { + override fun bindCurrentView(view: View, position: Int, data: ShiftObject) { + val descriptionTextView: TextView = view.findViewById(R.id.location) + val dateTextView: TextView = view.findViewById(R.id.date) + val totalPay: TextView = view.findViewById(R.id.total_pay) + val hoursView: TextView = view.findViewById(R.id.hours) + val h: TextView = view.findViewById(R.id.h) + val minutesView: TextView = view.findViewById(R.id.minutes) + val m: TextView = view.findViewById(R.id.m) + val editView: ImageView = view.findViewById(R.id.imageView) + h.text = "h" + m.text = "m" + val typeText: String = data.type + val descriptionText: String = data.description + val dateText: String = data.date + val totalPayText: String = data.totalPay.toString() + + descriptionTextView.text = descriptionText + dateTextView.text = dateText + totalPay.text = totalPayText + + when (ShiftType.getEnumByType(typeText)) { + ShiftType.HOURLY -> { + val time = data.getHoursMinutesPairFromDuration() + + hoursView.text = time.first + minutesView.text = time.second + } + + ShiftType.PIECE -> { + val unitsText: String = data.units.toString() + + hoursView.text = unitsText + h.text = "" + minutesView.text = "" + m.text = "pcs" + } + } + + val b: Bundle = Bundle() + b.putLong(ID, data.id) + view.setOnClickListener { + // Navigate to further info + fragment.navigateToFragment( + FurtherInfoFragment(), + bundle = b, + name = "furtherinfo" + ) + } + editView.setOnClickListener { + // Navigate to edit + fragment.navigateToFragment( + FragmentAddItem(), + bundle = b, + name = "additem" + ) + } + view.setOnLongClickListener { + AlertDialog.Builder(it.context) + .setMessage("Are you sure you want to delete") + .setPositiveButton("delete") { dialog, id -> longPressCallback.invoke(data.id) } + .setNegativeButton("cancel") { dialog, id -> + dialog?.dismiss() + } + .create().show() + true + } + } + +// override fun getItemId(position: Int): Long { +// return if (list.isNullOrEmpty()) { +// RecyclerView.NO_ID +// } else { +// list!![position].id +// } +// +// } +// +// override fun setHasStableIds(hasStableIds: Boolean) { +// super.setHasStableIds(true) +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt similarity index 94% rename from app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt rename to app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index 878fb98..5310bdb 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr +package com.appttude.h_mal.farmr.ui import android.app.Activity import android.content.Intent @@ -7,6 +7,7 @@ import android.os.Handler import android.view.View import android.widget.RelativeLayout import androidx.core.app.ActivityOptionsCompat +import com.appttude.h_mal.farmr.R /** * Created by h_mal on 27/06/2017. diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt new file mode 100644 index 0000000..59f48f8 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt @@ -0,0 +1,7 @@ +package com.appttude.h_mal.farmr.utils + +const val LEGACY = "LEGACY_" +const val DATE_FORMAT = "yyyy-MM-dd" +const val TIME_FORMAT = "hh:mm" +const val ID = "ID" +const val CURRENCY = "£" \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt new file mode 100644 index 0000000..1877d8d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -0,0 +1,81 @@ +package com.appttude.h_mal.farmr.utils + +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +fun String.formatToTwoDp(): Float { + val formattedString = String.format("%.2f", this) + return formattedString.toFloat() +} + +fun Float.formatToTwoDp(): Float { + val formattedString = String.format("%.2f", this) + return formattedString.toFloat() +} + +fun Float.formatToTwoDpString(): String { + return formatToTwoDp().toString() +} + +fun String.dateStringIsValid(): Boolean { + return DATE_FORMAT.toPattern().matcher(this).matches() +} + +fun String.timeStringIsValid(): Boolean { + return TIME_FORMAT.toPattern().matcher(this).matches() +} + +fun Calendar.getTimeString(): String { + val format = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()) + return format.format(time) +} + +/** + * turns "HH:mm" into an hour and minutes pair + * + * eg: + * @param 13:45 + * @return Pair(13, 45) + */ +fun convertTimeStringToHourMinutesPair(timeString: String): Pair { + val split = timeString.split(":") + if (split.size != 2) throw ArrayIndexOutOfBoundsException() + return Pair(split.first().toInt(), split[1].toInt()) +} + + +/** + * calculate the duration between two 24 hour time strings minus the break in minutes + * + * can also calculate when time to string in past midnight eg: 23:00, 04:45, 30 + * @return 5.75 + */ +fun calculateDuration(timeIn: String, timeOut: String, breaks: Int): Float { + val timeFrom = convertTimeStringToHourMinutesPair(timeIn) + val timeTo = convertTimeStringToHourMinutesPair(timeOut) + + val hoursIn = timeFrom.first + val minutesIn = timeFrom.second + val hoursOut = timeTo.first + val minutesOut = timeTo.second + + var duration: Float = if (hoursOut > hoursIn) { + ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + } else { + (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24) + } + if ((breaks.toFloat() / 60) > duration) throw IOException("Breaks duration cannot be larger than shift duration") + duration -= (breaks.toFloat() / 60) + + return duration.formatToTwoDp() +} + +fun calculateDuration(timeIn: String?, timeOut: String?, breaks: Int?): Float { + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + + return calculateDuration(insertTimeIn, insertTimeOut, breaks ?: 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt new file mode 100644 index 0000000..cb0d32c --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt @@ -0,0 +1,38 @@ +package com.appttude.h_mal.farmr.utils + +import com.appttude.h_mal.farmr.model.Order +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +@Suppress("UNCHECKED_CAST") +fun Any.getGenericClassAt(position: Int): KClass = + ((javaClass.genericSuperclass as? ParameterizedType) + ?.actualTypeArguments?.getOrNull(position) as? Class) + ?.kotlin + ?: throw IllegalStateException("Can not find class from generic argument") + +/** + * @param validate when result is false then we trigger + * @param onError + * + * + * @sample + * var s: String? + * i.validate{!i.isNullOrEmpty()} { print("string is empty") } + */ +inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Unit) { + if (!validate.invoke(this)) { + onError.invoke() + } +} + +/** + * Returns a list of all elements sorted according to the specified comparator. In order of ascending or descending + * The sort is stable. It means that equal elements preserve their order relative to each other after sorting. + */ +inline fun > Iterable.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List { + return when (order) { + Order.ASCENDING -> sortedWith(compareBy(selector)) + Order.DESCENDING -> sortedWith(compareByDescending(selector)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt new file mode 100644 index 0000000..8432a0e --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt @@ -0,0 +1,177 @@ +package com.appttude.h_mal.farmr.utils + +import android.app.Activity +import android.app.AlertDialog +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.Toast +import androidx.annotation.AnimRes +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentTransaction +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.ui.FragmentAddItem +import java.util.Calendar + +fun View.show() { + this.visibility = View.VISIBLE +} + +fun View.hide() { + this.visibility = View.GONE +} + +fun Context.displayToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() +} + +fun Fragment.displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() +} + +fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater + .from(context) + .inflate(layoutId, this, false) + +fun Fragment.hideKeyboard() { + val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(view?.windowToken, 0) +} + +fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) { + val animation = AnimationUtils.loadAnimation(context, id) + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation) + override fun onAnimationStart(a: Animation?) {} + override fun onAnimationRepeat(a: Animation?) {} + }) + startAnimation(animation) +} + +fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "") { + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + fragmentTransaction.replace(container, fragment).addToBackStack(name).commit() +} + +fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "", bundle: Bundle) { + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + fragmentTransaction.replace(container, fragment.apply { arguments = bundle }).addToBackStack(name).commit() +} + +fun Context.createDialog( + title: String?, + message: String?, + displayCancel: Boolean = false, + displayOk: Boolean = true, + cancelCallback: DialogInterface.OnClickListener? = null, + okCallback: DialogInterface.OnClickListener? = null, +) { + val builder = AlertDialog.Builder(this) + title?.let { builder.setTitle(it) } + message?.let { builder.setMessage(it) } + if (displayCancel) { + builder.setNegativeButton(android.R.string.cancel, cancelCallback) + } + if (displayOk) { + builder.setPositiveButton(android.R.string.ok, okCallback) + } + + builder.create().show() +} + +fun AppCompatActivity.popBackStack() { + supportFragmentManager.popBackStack() +} + +fun EditText.setTimePicker(onSelected: (String) -> Unit) { + var mHoursOut: Int + var mMinutesOut: Int + + setOnClickListener { + val mCurrentTime by lazy { Calendar.getInstance() } + if (!text.isNullOrEmpty()) { + // EditText contains text - lets try set the parse the text + try { + val convertedString = convertTimeStringToHourMinutesPair(text.toString()) + mHoursOut = convertedString.first + mMinutesOut = convertedString.second + } catch (e: Exception) { + mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY] + mMinutesOut = mCurrentTime[Calendar.MINUTE] + } + } else { + mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY] + mMinutesOut = mCurrentTime[Calendar.MINUTE] + } + val mTimePicker = TimePickerDialog(this.context, + { _, selectedHour, selectedMinute -> + val ddTime = String.format("%02d", selectedHour) + ":" + String.format( + "%02d", + selectedMinute + ) + setText(ddTime) + onSelected.invoke(ddTime) + }, mHoursOut, mMinutesOut, true + ) //Yes 24 hour time + mTimePicker.setTitle("Select Time") + mTimePicker.show() + } +} + +fun EditText.setDatePicker(onSelected: (String) -> Unit) { + //To show current date in the datepicker + var mYear: Int + var mMonth: Int + var mDay: Int + + val mCurrentDate by lazy { Calendar.getInstance() } + + if (!text.isNullOrEmpty()) { + try { + val dateSplit = text.split("-") + + mYear = dateSplit[0].toInt() + mMonth = dateSplit[1].toInt() + mMonth = if (mMonth == 1) { + 0 + } else { + mMonth - 1 + } + mDay = dateSplit[2].toInt() + } catch (e: Exception) { + mYear = mCurrentDate[Calendar.YEAR] + mMonth = mCurrentDate[Calendar.MONTH] + mDay = mCurrentDate[Calendar.DAY_OF_MONTH] + } + } else { + mYear = mCurrentDate[Calendar.YEAR] + mMonth = mCurrentDate[Calendar.MONTH] + mDay = mCurrentDate[Calendar.DAY_OF_MONTH] + } + val mDatePicker = DatePickerDialog( + (this.context), + { datepicker, selectedyear, selectedmonth, selectedday -> + var currentMonth = selectedmonth + val dateString = StringBuilder().append(selectedyear).append("-") + .append(String.format("%02d", (currentMonth + 1.also { currentMonth = it }))) + .append("-") + .append(String.format("%02d", selectedday)) + .toString() + setText(dateString) + onSelected.invoke(dateString) + }, mYear, mMonth, mDay + ) + mDatePicker.setTitle("Select date") + mDatePicker.show() +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt new file mode 100644 index 0000000..16883c7 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt @@ -0,0 +1,22 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.appttude.h_mal.farmr.data.RepositoryImpl + + +class ApplicationViewModelFactory( + private val repository: RepositoryImpl +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + with(modelClass) { + return when { + isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository) + else -> throw IllegalArgumentException("Unknown ViewModel class") + } as T + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..6d7281e --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -0,0 +1,483 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.appttude.h_mal.farmr.base.BaseViewModel +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.TIME_IN +import com.appttude.h_mal.farmr.data.prefs.TIME_OUT +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.FilterStore +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.dateStringIsValid +import com.appttude.h_mal.farmr.utils.formatToTwoDp +import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.sortedByOrder +import com.appttude.h_mal.farmr.utils.timeStringIsValid +import java.io.IOException +import java.util.Calendar + + +class MainViewModel( + private val repository: Repository +) : BaseViewModel() { + + private val _shiftLiveData = MutableLiveData>() + val shiftLiveData: LiveData> = _shiftLiveData + + private var mSort: Sortable = Sortable.ID + private var mOrder: Order = Order.ASCENDING + + private var mFilterStore: FilterStore? = null + + private val observer = Observer> { + onSuccess(it.sortList(mSort, mOrder)) + } + + init { + // Load shifts into live data when view model has been instantiated + refreshLiveData() + shiftLiveData.observeForever(observer) + } + + override fun onCleared() { + shiftLiveData.removeObserver(observer) + super.onCleared() + } + + private fun List.sortList(sort: Sortable, order: Order): List { + return when (sort) { + Sortable.ID -> sortedByOrder(order) { it.id } + Sortable.TYPE -> sortedByOrder(order) { it.type } + Sortable.DATE -> sortedByOrder(order) { it.date } + Sortable.DESCRIPTION -> sortedByOrder(order) { it.description } + Sortable.DURATION -> sortedByOrder(order) { it.duration } + Sortable.UNITS -> sortedByOrder(order) { it.units } + Sortable.RATEOFPAY -> sortedByOrder(order) { it.rateOfPay } + Sortable.TOTALPAY -> sortedByOrder(order) { it.totalPay } + } + } + + fun getSortAndOrder(): Pair { + return Pair(mSort, mOrder) + } + + fun setSortAndOrder(sort: Sortable, order: Order = Order.ASCENDING) { + mSort = sort + mOrder = order + refreshLiveData() + } + + fun getInformation(): String { + var totalDuration = 0.0f + var countOfTypeH = 0 + var countOfTypeP = 0 + var totalUnits = 0f + var totalPay = 0f + val lines = _shiftLiveData.value?.size ?: 0 + _shiftLiveData.value?.forEach { + totalDuration += it.duration + when (ShiftType.getEnumByType(it.type)) { + ShiftType.HOURLY -> countOfTypeH += 1 + ShiftType.PIECE -> countOfTypeP += 1 + } + totalUnits += it.units + totalPay += it.totalPay + } + + return buildInfoString( + totalDuration, + countOfTypeH, + countOfTypeP, + totalUnits, + totalPay, + lines + ) + } + + fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) + + fun insertHourlyShift( + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay.toFloat() >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it > 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + doTry { + insertShiftIntoDatabase( + ShiftType.HOURLY, + description, + date, + rateOfPay.formatToTwoDp(), + timeIn, + timeOut, + breakMins, + null + ) + } + + } + + fun insertPieceRateShift( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) { + // Validate inputs from the edit texts + (description.length < 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + (units.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + + doTry { + insertShiftIntoDatabase( + type = ShiftType.PIECE, + description = description, + date = date, + rateOfPay = rateOfPay.formatToTwoDp(), + null, + null, + null, + units = units + ) + } + } + + fun updateShift( + id: Long, + type: String? = null, + description: String? = null, + date: String? = null, + rateOfPay: String? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: String? = null, + units: String? = null, + ) { + description?.let { + (it.length < 3).validateField { + onError("Description length should be longer") + return + } + } + date?.dateStringIsValid()?.validateField { + onError("Date format is invalid") + return + } + rateOfPay?.let { + (it.toFloat() >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + } + units?.let { + (it.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it.toInt() > 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + doTry { + updateShiftInDatabase( + id, + type = type?.let { ShiftType.getEnumByType(it) }, + description = description, + date = date, + rateOfPay = rateOfPay?.toFloatOrNull(), + timeIn = timeIn, + timeOut = timeOut, + breakMins = breakMins?.toIntOrNull(), + units = units?.toFloatOrNull() + ) + } + } + + fun deleteShift(id: Long) { + if (!repository.deleteSingleShiftFromDatabase(id)) { + onError("Failed to delete shift") + } else { + refreshLiveData() + } + } + + fun deleteAllShifts() { + if (!repository.deleteAllShiftsFromDatabase()) { + onError("Failed to delete all shifts from database") + } else { + refreshLiveData() + } + } + + private fun updateShiftInDatabase( + id: Long, + type: ShiftType? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ) { + val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() + ?: throw IOException("Cannot update shift as it does not exist") + + val shift = when (type) { + ShiftType.HOURLY -> { + // Shift type has changed so mandatory fields for hourly shift are now required as well + val insertTimeIn = + (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") + val insertTimeOut = + (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + // Shift type has changed so mandatory fields for piece rate shift are now required as well + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + else -> { + if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { + // Updates to description or date field + currentShift.copy( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + ) + } else { + // Updating shifts where shift type has remained the same + when (currentShift.type) { + ShiftType.HOURLY -> { + val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( + "No time in inserted" + ) + val insertTimeOut = (timeOut ?: currentShift.timeOut) + ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + } + } + } + } + + repository.updateShiftIntoDatabase(id, shift) + } + + private fun insertShiftIntoDatabase( + type: ShiftType, + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + units: Float?, + ) { + val shift = when (type) { + ShiftType.HOURLY -> { + if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + + Shift( + description = description, + date = date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins, + rateOfPay = rateOfPay + ) + } + + ShiftType.PIECE -> { + Shift( + description = description, + date = date, + units = units!!, + rateOfPay = rateOfPay, + ) + } + } + + repository.insertShiftIntoDatabase(shift) + } + + + private fun buildInfoString( + totalDuration: Float, + countOfHourly: Int, + countOfPiece: Int, + totalUnits: Float, + totalPay: Float, + lines: Int + ): String { + var textString: String + textString = "$lines Shifts" + if (countOfHourly != 0 && countOfPiece != 0) { + textString = "$textString ($countOfHourly Hourly/$countOfPiece Piece Rate)" + } + if (countOfHourly != 0) { + textString = """ + $textString + Total Hours: ${String.format("%.2f", totalDuration)} + """.trimIndent() + } + if (countOfPiece != 0) { + textString = """ + $textString + Total Units: ${String.format("%.2f", totalUnits)} + """.trimIndent() + } + if (totalPay != 0f) { + textString = """ + $textString + Total Pay: ${"$"}${String.format("%.2f", totalPay)} + """.trimIndent() + } + return textString + } + + private fun refreshLiveData() { + _shiftLiveData.postValue(repository.readShiftsFromDatabase()) + } + + private inline fun Boolean.validateField(failureCallback: () -> Unit) { + if (!this) failureCallback.invoke() + } + + /** + * Lambda function that will invoke onError(...) on failure + * but update live data when successful + */ + private inline fun doTry(operation: () -> Unit) { + try { + operation.invoke() + refreshLiveData() + } catch (e: Exception) { + onError(e) + } + } + + fun setFiltrationDetails( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + } + + fun getFiltrationDetails(): FilterStore { + val prefs = repository.retrieveFilteringDetailsInPrefs() + mFilterStore = FilterStore( + prefs[DESCRIPTION], + prefs[TIME_IN], + prefs[TIME_OUT], + prefs[TYPE] + ) + return mFilterStore!! + } + + fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int): Float? { + try { + return calculateDuration(mTimeIn,mTimeOut,mBreaks) + }catch (e: IOException) { + onError(e) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/empty_list_view.xml b/app/src/main/res/layout/empty_list_view.xml new file mode 100644 index 0000000..8112b34 --- /dev/null +++ b/app/src/main/res/layout/empty_list_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_item.xml b/app/src/main/res/layout/fragment_add_item.xml index 8f6152b..fd3bf6f 100644 --- a/app/src/main/res/layout/fragment_add_item.xml +++ b/app/src/main/res/layout/fragment_add_item.xml @@ -8,16 +8,9 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.appttude.h_mal.farmr.FragmentAddItem" + tools:context="com.appttude.h_mal.farmr.ui.FragmentAddItem" android:orientation="vertical"> - - diff --git a/app/src/main/res/layout/fragment_filter_data.xml b/app/src/main/res/layout/fragment_filter_data.xml index 3b67fa9..3ebe08f 100644 --- a/app/src/main/res/layout/fragment_filter_data.xml +++ b/app/src/main/res/layout/fragment_filter_data.xml @@ -8,7 +8,7 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.appttude.h_mal.farmr.FilterDataFragment"> + tools:context="com.appttude.h_mal.farmr.ui.FilterDataFragment"> + tools:context="com.appttude.h_mal.farmr.ui.FurtherInfoFragment"> + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="com.appttude.h_mal.farmr.ui.FragmentMain"> - - - - - - - - - + + app:backgroundTint="@color/colorPrimary" /> diff --git a/app/src/main/res/layout/list_item_1.xml b/app/src/main/res/layout/list_item_1.xml index 3ff54df..bdec9ea 100644 --- a/app/src/main/res/layout/list_item_1.xml +++ b/app/src/main/res/layout/list_item_1.xml @@ -126,7 +126,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" - android:layout_alignParentTop="true" app:srcCompat="@android:drawable/ic_menu_edit" /> diff --git a/app/src/main/res/layout/main_view.xml b/app/src/main/res/layout/main_view.xml index 790957a..5dcff32 100644 --- a/app/src/main/res/layout/main_view.xml +++ b/app/src/main/res/layout/main_view.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 0135299..e4980fb 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -1,7 +1,7 @@ + tools:context="com.appttude.h_mal.farmr.ui.MainActivity"> Hello blank fragment Shift Details + insert break in minutes + Break From 22982f1482c03f39bab7079e9a813b4dd5bd84a7 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sat, 26 Aug 2023 21:43:35 +0100 Subject: [PATCH 03/15] - MVVM refactor - Kodein DI added - Overhaul dirty code in views - Bug fixes --- .../appttude/h_mal/farmr/base/BaseActivity.kt | 2 +- .../farmr/data/legacydb/LegacyDatabase.kt | 11 +-- .../h_mal/farmr/di/ShiftApplication.kt | 2 +- .../h_mal/farmr/ui/FragmentAddItem.kt | 68 +++++++++++++++---- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 12 +++- .../appttude/h_mal/farmr/ui/MainActivity.kt | 9 --- ...RecyclerAdapter.kt => ShiftListAdapter.kt} | 49 +++++++------ .../appttude/h_mal/farmr/utils/Formatting.kt | 4 +- .../appttude/h_mal/farmr/utils/ViewUtils.kt | 4 +- .../h_mal/farmr/viewmodel/MainViewModel.kt | 18 ++--- 10 files changed, 115 insertions(+), 64 deletions(-) rename app/src/main/java/com/appttude/h_mal/farmr/ui/{ShiftRecyclerAdapter.kt => ShiftListAdapter.kt} (71%) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt index 16a8384..3960cf5 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt @@ -53,6 +53,6 @@ abstract class BaseActivity : AppCompatActivity(), KodeinAwar } fun setTitleInActionBar(title: String) { - setTitle(title) + supportActionBar?.title = title } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt index c413f9f..57e019e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt @@ -1,5 +1,6 @@ package com.appttude.h_mal.farmr.data.legacydb +import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues import android.content.Context @@ -20,8 +21,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType -class LegacyDatabase(context: Context) { - private val resolver = context.contentResolver +class LegacyDatabase(private val resolver: ContentResolver) { private val projection = arrayOf( _ID, @@ -44,7 +44,7 @@ class LegacyDatabase(context: Context) { val values = ContentValues().apply { put(COLUMN_SHIFT_TYPE, shift.type.type) put(COLUMN_SHIFT_DESCRIPTION, shift.description) - put(COLUMN_SHIFT_DATE, shift.description) + put(COLUMN_SHIFT_DATE, shift.date) put(COLUMN_SHIFT_TIME_IN, shift.timeIn ?: "00:00") put(COLUMN_SHIFT_TIME_OUT, shift.timeOut ?: "00:00") put(COLUMN_SHIFT_DURATION, shift.duration ?: 0.00f) @@ -63,8 +63,9 @@ class LegacyDatabase(context: Context) { projection, null, null, null ) ?: return null - cursor.moveToFirst() - val shifts = (0..cursor.count).map { cursor.getShift() } + val shifts = generateSequence { if (cursor.moveToNext()) cursor else null } + .map { it.getShift() } + .toList() // close cursor after query operations cursor.close() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt index ced4fb4..0e1532f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -19,7 +19,7 @@ class ShiftApplication: Application(), KodeinAware { override val kodein = Kodein.lazy { import(androidXModule(this@ShiftApplication)) - bind() from singleton { LegacyDatabase(this@ShiftApplication) } + bind() from singleton { LegacyDatabase(contentResolver) } bind() from singleton { PreferenceProvider(this@ShiftApplication) } bind() from singleton { RepositoryImpl(instance(), instance()) } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index f201bf1..1454eb8 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -51,11 +51,13 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), private var mDescription: String? = null private var mTimeIn: String? = null private var mTimeOut: String? = null - private var mBreaks = 0 - private var mUnits = 0f + private var mBreaks: Int? = null + private var mUnits: Float? = null private var mPayRate = 0f private var mType: ShiftType? = null - private var mDuration: Float = 0f + private var mDuration: Float? = null + + private var id: Long? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -97,7 +99,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), calculateTotalPay() } mUnitEditText.doAfterTextChanged { - it.toString().toFloatOrNull()?.let { u -> mPayRate = u } + it.toString().toFloatOrNull()?.let { u -> mUnits = u } calculateTotalPay() } mPayRateEditText.doAfterTextChanged { @@ -113,6 +115,8 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), } private fun setupViewAfterViewCreated() { + id = arguments?.getLong(ID) + val title = when (arguments?.containsKey(ID)) { true -> { // Since we are editing a shift lets load the shift data into the views @@ -158,6 +162,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), hourlyDataView.show() durationHolder.show() } + R.id.piecerate -> { mType = ShiftType.PIECE wholeView.show() @@ -169,27 +174,62 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), } private fun submitShift() { - mDate.validateField({ !it.isNullOrBlank() }){ + mDate.validateField({ !it.isNullOrBlank() }) { onFailure("Date field cannot be empty") return } - mDescription.validateField({ !it.isNullOrBlank() }){ + mDescription.validateField({ !it.isNullOrBlank() }) { onFailure("Description field cannot be empty") return } - mPayRate.validateField({ !it.isNaN() }){ + mPayRate.validateField({ !it.isNaN() }) { onFailure("Rate of pay field cannot be empty") return } if (mPieceRadioButton.isChecked) { - mUnits.validateField({!it.isNaN()}) { + + mUnits.validateField({ it != null && it >= 0 }) { onFailure("Units field cannot be empty") return } - viewModel.insertPieceRateShift(mDescription!!, mDate!!, mUnits, mPayRate) + if (id != null) { + // update + viewModel.updateShift( + id!!, + description = mDescription, + date = mDate, + units = mUnits, + rateOfPay = mPayRate + ) + } else { + // insert + viewModel.insertPieceRateShift(mDescription!!, mDate!!, mUnits!!, mPayRate) + } } else if (mHourlyRadioButton.isChecked) { - viewModel.insertHourlyShift(mDescription!!, mDate!!, mPayRate, mTimeIn, mTimeOut, mBreaks) + if (id != null) { + // update + viewModel.updateShift( + id!!, + description = mDescription, + date = mDate, + rateOfPay = mPayRate, + timeIn = mTimeIn, + timeOut = mTimeOut, + breakMins = mBreaks + ) + } else { + // insert + viewModel.insertHourlyShift( + mDescription!!, + mDate!!, + mPayRate, + mTimeIn, + mTimeOut, + mBreaks + ) + } + } } @@ -199,11 +239,13 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), ShiftType.HOURLY -> { // Calculate duration before total pay calculation mDuration = viewModel.retrieveDurationText(mTimeIn, mTimeOut, mBreaks) ?: return - mDurationTextView.text = StringBuilder().append(mDuration).append(" hours").toString() - mDuration * mPayRate + mDurationTextView.text = + StringBuilder().append(mDuration).append(" hours").toString() + mDuration!! * mPayRate } + ShiftType.PIECE -> { - mUnits * mPayRate + (mUnits ?: 0f) * mPayRate } } mTotalPayTextView.text = total.formatToTwoDpString() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index 6d053d6..b4f557b 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -27,7 +27,7 @@ import kotlin.system.exitProcess class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { lateinit var activity: MainActivity private lateinit var productListView: RecyclerView - private lateinit var mAdapter: ShiftRecyclerAdapter + private lateinit var mAdapter: ShiftListAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,7 +38,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mAdapter = ShiftRecyclerAdapter(this) { + mAdapter = ShiftListAdapter(this) { viewModel.deleteShift(it) } productListView = view.findViewById(R.id.list_item_view) @@ -49,10 +49,16 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } } + override fun onStart() { + super.onStart() + + viewModel.refreshLiveData() + } + override fun onSuccess(data: Any?) { super.onSuccess(data) if (data is List<*>) { - mAdapter.updateData(data as List) + mAdapter.submitList(data as List) } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt index 87bb453..187d4cd 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -22,10 +22,6 @@ import kotlin.system.exitProcess class MainActivity : BaseActivity() { private lateinit var toolbar: Toolbar - var selection: String? = null - var args: Array? = null - - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_view) @@ -57,11 +53,6 @@ class MainActivity : BaseActivity() { } } - - fun setActionBarTitle(title: String?) { - toolbar.title = title - } - // Storage Permissions private val REQUEST_EXTERNAL_STORAGE = 1 private val PERMISSIONS_STORAGE = arrayOf( diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt similarity index 71% rename from app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt rename to app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt index acb99da..59d8987 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftRecyclerAdapter.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt @@ -2,26 +2,36 @@ package com.appttude.h_mal.farmr.ui import android.app.AlertDialog import android.os.Bundle -import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.generateView import com.appttude.h_mal.farmr.utils.navigateToFragment -class ShiftRecyclerAdapter( +class ShiftListAdapter( private val fragment: Fragment, private val longPressCallback: (Long) -> Unit -) : BaseRecyclerAdapter( - emptyViewId = R.layout.empty_list_view, - currentViewId = R.layout.list_item_1 -) { - override fun bindCurrentView(view: View, position: Int, data: ShiftObject) { +) : ListAdapter(diffCallBack) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseRecyclerAdapter.CurrentViewHolder { + val currentViewHolder = parent.generateView(R.layout.list_item_1) + return BaseRecyclerAdapter.CurrentViewHolder(currentViewHolder) + } + + override fun onBindViewHolder(holder: BaseRecyclerAdapter.CurrentViewHolder, position: Int) { + val view = holder.itemView + val data = getItem(position) + val descriptionTextView: TextView = view.findViewById(R.id.location) val dateTextView: TextView = view.findViewById(R.id.date) val totalPay: TextView = view.findViewById(R.id.total_pay) @@ -89,16 +99,15 @@ class ShiftRecyclerAdapter( } } -// override fun getItemId(position: Int): Long { -// return if (list.isNullOrEmpty()) { -// RecyclerView.NO_ID -// } else { -// list!![position].id -// } -// -// } -// -// override fun setHasStableIds(hasStableIds: Boolean) { -// super.setHasStableIds(true) -// } + companion object { + val diffCallBack = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { + return oldItem == newItem + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt index 1877d8d..7be48db 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -20,11 +20,11 @@ fun Float.formatToTwoDpString(): String { } fun String.dateStringIsValid(): Boolean { - return DATE_FORMAT.toPattern().matcher(this).matches() + return "([0-9]{4})-([0-9]{2})-([0-9]{2})".toPattern().matcher(this).matches() } fun String.timeStringIsValid(): Boolean { - return TIME_FORMAT.toPattern().matcher(this).matches() + return "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]\$".toPattern().matcher(this).matches() } fun Calendar.getTimeString(): String { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt index 8432a0e..659b798 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt @@ -173,5 +173,7 @@ fun EditText.setDatePicker(onSelected: (String) -> Unit) { }, mYear, mMonth, mDay ) mDatePicker.setTitle("Select date") - mDatePicker.show() + setOnClickListener { + mDatePicker.show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index 6d7281e..08d1185 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -160,7 +160,7 @@ class MainViewModel( rateOfPay: Float ) { // Validate inputs from the edit texts - (description.length < 3).validateField { + (description.length > 3).validateField { onError("Description length should be longer") return } @@ -196,11 +196,11 @@ class MainViewModel( type: String? = null, description: String? = null, date: String? = null, - rateOfPay: String? = null, + rateOfPay: Float? = null, timeIn: String? = null, timeOut: String? = null, - breakMins: String? = null, - units: String? = null, + breakMins: Int? = null, + units: Float? = null, ) { description?.let { (it.length < 3).validateField { @@ -243,11 +243,11 @@ class MainViewModel( type = type?.let { ShiftType.getEnumByType(it) }, description = description, date = date, - rateOfPay = rateOfPay?.toFloatOrNull(), + rateOfPay = rateOfPay, timeIn = timeIn, timeOut = timeOut, - breakMins = breakMins?.toIntOrNull(), - units = units?.toFloatOrNull() + breakMins = breakMins, + units = units ) } } @@ -430,7 +430,7 @@ class MainViewModel( return textString } - private fun refreshLiveData() { + fun refreshLiveData() { _shiftLiveData.postValue(repository.readShiftsFromDatabase()) } @@ -471,7 +471,7 @@ class MainViewModel( return mFilterStore!! } - fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int): Float? { + fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { try { return calculateDuration(mTimeIn,mTimeOut,mBreaks) }catch (e: IOException) { From 2aaeaff22c1f8c176611ee0cd2c360b1c492efda Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sat, 26 Aug 2023 21:45:15 +0100 Subject: [PATCH 04/15] - LegacyDatabaseTest.kt added --- .../farmr/data/legacydb/LegacyDatabaseTest.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt new file mode 100644 index 0000000..e539bdd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt @@ -0,0 +1,91 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import androidx.test.rule.provider.ProviderTestRule +import com.appttude.h_mal.farmr.model.Shift +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class LegacyDatabaseTest { + @get:Rule + val providerRule: ProviderTestRule = ProviderTestRule + .Builder(ShiftProvider::class.java, ShiftsContract.CONTENT_AUTHORITY) + .build() + + private lateinit var database: LegacyDatabase + + @Before + fun setup() { + database = LegacyDatabase(providerRule.resolver) + } + + @After + fun tearDown() { + database.deleteAllShiftsInDatabase() + } + + @Test + fun insertShift_readShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val retrievedShift = database.readShiftsFromDatabase()?.first() + + // Assert + assertEquals(retrievedShift?.description, shift.description) + assertEquals(retrievedShift?.date, shift.date) + assertEquals(retrievedShift?.units, shift.units) + assertEquals(retrievedShift?.rateOfPay, shift.rateOfPay) + } + + @Test + fun insertShift_updateShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.updateShiftDataIntoDatabase( + id = id, + typeString = updateShift.type.type, + descriptionString = updateShift.description, + dateString = updateShift.date, + timeInString = updateShift.timeIn ?: "", + timeOutString = updateShift.timeOut ?: "", + duration = updateShift.duration ?: 0f, + breaks = updateShift.breakMins ?: 0, + units = updateShift.units!!, + payRate = updateShift.rateOfPay, + totalPay = updateShift.totalPay + ) + val retrievedShift = database.readSingleShiftWithId(id) + + // Assert + assertEquals(retrievedShift?.description, updateShift.description) + assertEquals(retrievedShift?.date, updateShift.date) + assertEquals(retrievedShift?.units, updateShift.units) + assertEquals(retrievedShift?.rateOfPay, updateShift.rateOfPay) + } + + @Test + fun insertShift_deleteShift_databaseEmpty() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + database.insertShiftDataIntoDatabase(updateShift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.deleteSingleShift(id) + + // Assert + assertEquals(database.readShiftsFromDatabase()?.size, 1) + } +} \ No newline at end of file From 68f02b8917630e98a930a462f16ef9ac7ee538f4 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sat, 26 Aug 2023 22:23:19 +0100 Subject: [PATCH 05/15] - RepositoryImplTest.kt added --- app/build.gradle | 28 +++++++++- .../appttude/h_mal/farmr/ExampleUnitTest.java | 17 ------ .../h_mal/farmr/data/RepositoryImplTest.kt | 52 +++++++++++++++++++ .../appttude/h_mal/farmr/utils/testUtils.kt | 39 ++++++++++++++ 4 files changed, 117 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 8a92332..e7edb61 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,11 +34,33 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.preference:preference:1.2.1' - implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' - testImplementation 'junit:junit:4.12' + / * Unit testing * / + testImplementation 'junit:junit:4.13.2' + androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' + / * mockito and livedata testing * / + testImplementation 'org.mockito:mockito-inline:2.13.0' + implementation 'androidx.arch.core:core-testing:2.1.0' + / * MockK * / + def mockk_ver = "1.10.5" + testImplementation "io.mockk:mockk:$mockk_ver" + androidTestImplementation "io.mockk:mockk-android:$mockk_ver" + / * Android Espresso * / + def testJunitVersion = "1.1.5" + def testRunnerVersion = "1.5.2" + def espressoVersion = "3.5.1" + androidTestImplementation "androidx.test.ext:junit:$testJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion" + implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion" + androidTestImplementation "androidx.test:runner:$testRunnerVersion" + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + androidTestImplementation "org.hamcrest:hamcrest:2.2" / * Room database * / def room_version = "2.4.3" implementation "androidx.room:room-runtime:$room_version" @@ -48,4 +70,6 @@ dependencies { def kodein_version = "6.2.1" implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" + / * SQLite to excel */ + implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' } diff --git a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java b/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java deleted file mode 100644 index cfb3bfd..0000000 --- a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.appttude.h_mal.farmr; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt new file mode 100644 index 0000000..4c53925 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt @@ -0,0 +1,52 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyLong +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class RepositoryImplTest { + + private lateinit var repository: RepositoryImpl + + @MockK + lateinit var db: LegacyDatabase + + @MockK + lateinit var prefs: PreferenceProvider + + @Before + fun setUp() { + MockKAnnotations.init(this) + repository = RepositoryImpl(db, prefs) + } + + @Test + fun readDatabase_validResponse() { + // Arrange + val elements = listOf( + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() } + ) + + //Act + every { db.readShiftsFromDatabase() } returns elements + + // Assert + val result = repository.readShiftsFromDatabase() + assertIs>(result) + assertEquals(result.first().id, anyLong()) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt new file mode 100644 index 0000000..4d64b8d --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt @@ -0,0 +1,39 @@ +package com.appttude.h_mal.farmr.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} + +fun sleep(millis: Long = 1000) { + runBlocking(Dispatchers.Default) { delay(millis) } +} \ No newline at end of file From c6316ca910361d8c60fc7609c681a71272395a37 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sun, 27 Aug 2023 23:17:52 +0100 Subject: [PATCH 06/15] - Shift list exportation completed --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 10 + .../appttude/h_mal/farmr/base/BaseFragment.kt | 3 + .../com/appttude/h_mal/farmr/model/Shift.kt | 2 +- .../appttude/h_mal/farmr/model/ShiftType.kt | 4 - .../com/appttude/h_mal/farmr/model/Success.kt | 5 + .../h_mal/farmr/ui/FilterDataFragment.kt | 7 +- .../h_mal/farmr/ui/FragmentAddItem.kt | 26 +++ .../appttude/h_mal/farmr/ui/FragmentMain.kt | 191 +++------------- .../appttude/h_mal/farmr/utils/Formatting.kt | 6 + .../h_mal/farmr/viewmodel/MainViewModel.kt | 204 +++++++++++++++--- app/src/main/res/xml/provider_path.xml | 4 + 12 files changed, 269 insertions(+), 198 deletions(-) create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt create mode 100644 app/src/main/res/xml/provider_path.xml diff --git a/app/build.gradle b/app/build.gradle index 8a92332..dafe122 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,6 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.preference:preference:1.2.1' - - implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' @@ -48,4 +46,7 @@ dependencies { def kodein_version = "6.2.1" implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" + / * jxl * / + implementation 'net.sourceforge.jexcelapi:jxl:2.6.12' + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12b8ee2..5b13a45 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,16 @@ android:name="com.appttude.h_mal.farmr.data.legacydb.ShiftProvider" android:authorities="com.appttude.h_mal.farmr" android:exported="false" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt index affb4b0..a939028 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.createViewModelLazy import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getGenericClassAt +import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import org.kodein.di.KodeinAware import org.kodein.di.android.x.kodein @@ -76,4 +77,6 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) fun setTitle(title: String) { (requireActivity() as BaseActivity<*>).setTitleInActionBar(title) } + + fun popBackStack() = mActivity?.popBackStack() } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 3d8e5ab..2f87159 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -39,7 +39,7 @@ data class Shift( timeOut, duration, breakTime, - duration, + 0f, rateOfPay, (duration * rateOfPay).formatToTwoDp() ) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt index b724c19..4087686 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt @@ -4,10 +4,6 @@ enum class ShiftType(val type: String){ HOURLY("Hourly"), PIECE("Piece Rate"); - fun getEnumByType(type: String): ShiftType { - return values().first { it.type == type } - } - companion object { fun getEnumByType(type: String): ShiftType { return values().first { it.type == type } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt new file mode 100644 index 0000000..b7b3d9d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.model + +data class Success( + val successMessage: String +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt index ca917f3..6611c31 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -12,6 +12,7 @@ import androidx.core.widget.doAfterTextChanged import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.setDatePicker import com.appttude.h_mal.farmr.viewmodel.MainViewModel @@ -32,7 +33,6 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setTitle(getString(R.string.title_activity_filter_data)) LocationET = view.findViewById(R.id.filterLocationEditText) @@ -89,4 +89,9 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ override fun onClick(p0: View?) { submitFiltrationDetails() } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) popBackStack() + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index 1454eb8..eb74f10 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -14,8 +14,10 @@ import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.ID import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.displayToast import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.popBackStack @@ -123,6 +125,13 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), viewModel.getCurrentShift(arguments!!.getLong(ID))?.run { mLocationEditText.setText(description) mDateEditText.setText(date) + + // Set types + mType = ShiftType.getEnumByType(type) + mDescription = description + mDate = date + mPayRate = rateOfPay + when (ShiftType.getEnumByType(type)) { ShiftType.HOURLY -> { mHourlyRadioButton.isChecked = true @@ -132,16 +141,26 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), mBreakEditText.setText(breakMins.toString()) val durationText = "${duration.formatToTwoDpString()} Hours" mDurationTextView.text = durationText + + // Set fields + mTimeIn = timeIn + mTimeOut = timeOut + mBreaks = breakMins } ShiftType.PIECE -> { mHourlyRadioButton.isChecked = false mPieceRadioButton.isChecked = true mUnitEditText.setText(units.formatToTwoDpString()) + + // Set piece rate units + mUnits = units } } mPayRateEditText.setText(rateOfPay.formatToTwoDpString()) mTotalPayTextView.text = totalPay.formatToTwoDpString() + + calculateTotalPay() } // Return title @@ -268,4 +287,11 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), return true } + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) { + displayToast(data.successMessage) + popBackStack() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index b4f557b..f0bfaf8 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -6,11 +6,13 @@ import android.app.Activity import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider import androidx.recyclerview.widget.RecyclerView import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener @@ -22,15 +24,17 @@ import com.appttude.h_mal.farmr.utils.createDialog import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.viewmodel.MainViewModel import com.google.android.material.floatingactionbutton.FloatingActionButton +import java.io.File import kotlin.system.exitProcess + class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { - lateinit var activity: MainActivity private lateinit var productListView: RecyclerView private lateinit var mAdapter: ShiftListAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setTitle("Shift List") // Inflate the layout for this fragment setHasOptionsMenu(true) } @@ -58,6 +62,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr override fun onSuccess(data: Any?) { super.onSuccess(data) if (data is List<*>) { + @Suppress("UNCHECKED_CAST") mAdapter.submitList(data as List) } } @@ -79,12 +84,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } R.id.filter_data -> { -// val fragmentTransaction: FragmentTransaction = -// activity.fragmentManager!!.beginTransaction() -// fragmentTransaction.replace(R.id.container, FilterDataFragment()) -// .addToBackStack("filterdata").commit() - // Todo: filter shift - + navigateToFragment(FilterDataFragment(), name = "filterdata") return true } @@ -92,8 +92,9 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr sortData() return true } + R.id.clear_filter -> { - // Todo: Apply filter to list + viewModel.setFiltrationDetails(null, null, null, null) return true } @@ -103,7 +104,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() } + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() } .create().show() } else { Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT) @@ -159,157 +160,29 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr ) } - private fun ExportData() { -// val permission = -// ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) -// if (permission != PackageManager.PERMISSION_GRANTED) { -// Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() -// return -// } -// shiftsDbhelper = ShiftsDbHelper(context) -// val database = shiftsDbhelper!!.writableDatabase -// val projection_export = arrayOf( -// ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, -// ShiftsEntry.COLUMN_SHIFT_DATE, -// ShiftsEntry.COLUMN_SHIFT_TIME_IN, -// ShiftsEntry.COLUMN_SHIFT_TIME_OUT, -// ShiftsEntry.COLUMN_SHIFT_BREAK, -// ShiftsEntry.COLUMN_SHIFT_DURATION, -// ShiftsEntry.COLUMN_SHIFT_TYPE, -// ShiftsEntry.COLUMN_SHIFT_UNIT, -// ShiftsEntry.COLUMN_SHIFT_PAYRATE, -// ShiftsEntry.COLUMN_SHIFT_TOTALPAY -// ) -// val cursor = activity.contentResolver.query( -// ShiftsEntry.CONTENT_URI, -// projection_export, -// activity.selection, -// activity.args, -// activity.sortOrder -// ) -// database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null) -// var totalDuration = 0.00f -// var totalUnits = 0.00f -// var totalPay = 0.00f -// try { -// while (cursor!!.moveToNext()) { -// val descriptionColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) -// val dateColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) -// val timeInColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) -// val timeOutColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) -// val durationColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) -// val breakOutColumnIndex = -// cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK)) -// val typeColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) -// val unitColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) -// val payrateColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE)) -// val totalpayColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) -// totalUnits = totalUnits + unitColumnIndex -// totalDuration = totalDuration + durationColumnIndex -// totalPay = totalPay + totalpayColumnIndex -// val values = ContentValues() -// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex) -// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) -// } -// } catch (e: Exception) { -// Log.e("FragmentMain", "ExportData: ", e) -// } finally { -// val values = ContentValues() -// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:") -// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration) -// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:") -// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits) -// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:") -// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) -// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) -// cursor!!.close() -// } -// val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp" -// val file = File(savePath) -// if (!file.exists()) { -// file.mkdirs() -// } -// val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath) -// sqLiteToExcel.exportSingleTable( -// "shiftsexport", -// "shifthistory.xls", -// object : ExportListener { -// override fun onStart() {} -// override fun onCompleted(filePath: String) { -// Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show() -// val newPath = Uri.parse("file://$savePath/shifthistory.xls") -// val builder = VmPolicy.Builder() -// StrictMode.setVmPolicy(builder.build()) -// val emailintent = Intent(Intent.ACTION_SEND) -// emailintent.type = "application/vnd.ms-excel" -// emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts") -// emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.") -// emailintent.putExtra(Intent.EXTRA_STREAM, newPath) -// startActivity(Intent.createChooser(emailintent, "Send Email")) -// } -// -// override fun onError(e: Exception) { -// println("Error msg: $e") -// Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show() -// } -// }) - } + private fun exportData() { + val permission = + ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) + if (permission != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() + return + } - @SuppressLint("DefaultLocale") - fun buildInfoString( - totalDuration: Float, - countOfTypeH: Int, - countOfTypeP: Int, - totalUnits: Float, - totalPay: Float, - lines: Int - ): String { - var textString: String - textString = "$lines Shifts" - if (countOfTypeH != 0 && countOfTypeP != 0) { - textString = "$textString ($countOfTypeH Hourly/$countOfTypeP Piece Rate)" + val fileName = "shifthistory.xls" + val file = File(requireContext().externalCacheDir, fileName) + + viewModel.createExcelSheet(file)?.let { + val intent = Intent(Intent.ACTION_VIEW) + val excelUri = FileProvider.getUriForFile( + requireContext(), + requireContext().applicationContext.packageName + ".provider", + file + ) + intent.setDataAndType(excelUri, "application/vnd.ms-excel") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent) } - if (countOfTypeH != 0) { - textString = """ - $textString - Total Hours: ${String.format("%.2f", totalDuration)} - """.trimIndent() - } - if (countOfTypeP != 0) { - textString = """ - $textString - Total Units: ${String.format("%.2f", totalUnits)} - """.trimIndent() - } - if (totalPay != 0f) { - textString = """ - $textString - Total Pay: ${"$"}${String.format("%.2f", totalPay)} - """.trimIndent() - } - return textString + } override fun onRequestPermissionsResult( @@ -335,7 +208,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() }.create().show() } fun checkStoragePermissions(activity: Activity?): Boolean { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt index 7be48db..00f9119 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -3,6 +3,7 @@ package com.appttude.h_mal.farmr.utils import java.io.IOException import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Date import java.util.Locale fun String.formatToTwoDp(): Float { @@ -32,6 +33,11 @@ fun Calendar.getTimeString(): String { return format.format(time) } +fun String.convertDateString(format: String = DATE_FORMAT): Date? { + val formatter = SimpleDateFormat(format, Locale.getDefault()) + return formatter.parse(this) +} + /** * turns "HH:mm" into an hour and minutes pair * diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index 08d1185..5c79752 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -1,11 +1,26 @@ package com.appttude.h_mal.farmr.viewmodel +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import android.os.Build +import android.os.Environment +import androidx.annotation.RequiresPermission import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.appttude.h_mal.farmr.base.BaseViewModel import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.TIME_IN import com.appttude.h_mal.farmr.data.prefs.TIME_OUT @@ -15,14 +30,24 @@ import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.CURRENCY import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.convertDateString import com.appttude.h_mal.farmr.utils.dateStringIsValid import com.appttude.h_mal.farmr.utils.formatToTwoDp import com.appttude.h_mal.farmr.utils.getTimeString import com.appttude.h_mal.farmr.utils.sortedByOrder import com.appttude.h_mal.farmr.utils.timeStringIsValid +import jxl.Workbook +import jxl.WorkbookSettings +import jxl.write.Label +import jxl.write.WritableWorkbook +import jxl.write.WriteException +import java.io.File import java.io.IOException import java.util.Calendar +import java.util.Locale class MainViewModel( @@ -38,7 +63,8 @@ class MainViewModel( private var mFilterStore: FilterStore? = null private val observer = Observer> { - onSuccess(it.sortList(mSort, mOrder)) + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) } init { @@ -47,6 +73,47 @@ class MainViewModel( shiftLiveData.observeForever(observer) } + private fun List.applyFilters(): List { + val filter = getFiltrationDetails() + + return filter { s -> + comparedStrings(filter.type, s.type) && + comparedStringsContains(filter.description, s.description) && + (isBetween(filter.dateFrom, filter.dateTo, s.date) ?: true) + } + } + + private fun comparedStrings(first: String?, second: String?): Boolean { + return when (compareValues(first, second)) { + -1, 0, 1 -> true + else -> { + false + } + } + } + + private fun comparedStringsContains(first: String?, second: String?): Boolean { + first?.let { + (second?.contains(it))?.let { c -> return c } + } + + return comparedStrings(first, second) + } + + private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? { + val first = fromDate?.convertDateString() + val second = toDate?.convertDateString() + + if (first == null && second == null) return null + val compareDate = compareWith.convertDateString() ?: return null + + if (second == null) return compareDate.after(first) + if (first == null) return compareDate.before(second) + + return compareDate.after(first) && compareDate.before(second) + } + + override fun onCleared() { shiftLiveData.removeObserver(observer) super.onCleared() @@ -121,7 +188,7 @@ class MainViewModel( onError("Date format is invalid") return } - (rateOfPay.toFloat() >= 0.00).validateField { + (rateOfPay >= 0.00).validateField { onError("Rate of pay is invalid") return } @@ -139,7 +206,7 @@ class MainViewModel( } doTry { - insertShiftIntoDatabase( + val result = insertShiftIntoDatabase( ShiftType.HOURLY, description, date, @@ -149,6 +216,8 @@ class MainViewModel( breakMins, null ) + + if (result) onSuccess(Success("Shift successfully added")) } } @@ -178,7 +247,7 @@ class MainViewModel( } doTry { - insertShiftIntoDatabase( + val result = insertShiftIntoDatabase( type = ShiftType.PIECE, description = description, date = date, @@ -188,6 +257,7 @@ class MainViewModel( null, units = units ) + if (result) onSuccess(Success("New shift successfully added")) } } @@ -203,7 +273,7 @@ class MainViewModel( units: Float? = null, ) { description?.let { - (it.length < 3).validateField { + (it.length > 3).validateField { onError("Description length should be longer") return } @@ -213,7 +283,7 @@ class MainViewModel( return } rateOfPay?.let { - (it.toFloat() >= 0.00).validateField { + (it >= 0.00).validateField { onError("Rate of pay is invalid") return } @@ -232,13 +302,13 @@ class MainViewModel( onError("Time out format is in correct") return } - breakMins?.let { it.toInt() > 0 }?.validateField { + breakMins?.let { it >= 0 }?.validateField { onError("Break in minutes is invalid") return } doTry { - updateShiftInDatabase( + val result = updateShiftInDatabase( id, type = type?.let { ShiftType.getEnumByType(it) }, description = description, @@ -249,6 +319,8 @@ class MainViewModel( breakMins = breakMins, units = units ) + + if (result) onSuccess(Success("Shift successfully updated")) } } @@ -278,7 +350,7 @@ class MainViewModel( timeOut: String? = null, breakMins: Int? = null, units: Float? = null, - ) { + ): Boolean { val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() ?: throw IOException("Cannot update shift as it does not exist") @@ -352,7 +424,7 @@ class MainViewModel( } } - repository.updateShiftIntoDatabase(id, shift) + return repository.updateShiftIntoDatabase(id, shift) } private fun insertShiftIntoDatabase( @@ -364,7 +436,7 @@ class MainViewModel( timeOut: String?, breakMins: Int?, units: Float?, - ) { + ): Boolean { val shift = when (type) { ShiftType.HOURLY -> { if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") @@ -392,7 +464,7 @@ class MainViewModel( } } - repository.insertShiftIntoDatabase(shift) + return repository.insertShiftIntoDatabase(shift) } @@ -404,30 +476,21 @@ class MainViewModel( totalPay: Float, lines: Int ): String { - var textString: String - textString = "$lines Shifts" + val stringBuilder = StringBuilder("$lines Shifts").append("\n") + if (countOfHourly != 0 && countOfPiece != 0) { - textString = "$textString ($countOfHourly Hourly/$countOfPiece Piece Rate)" + stringBuilder.append(" ($countOfHourly Hourly/$countOfPiece Piece Rate)").append("\n") } if (countOfHourly != 0) { - textString = """ - $textString - Total Hours: ${String.format("%.2f", totalDuration)} - """.trimIndent() + stringBuilder.append("Total Hours: ").append(totalDuration).append("\n") } if (countOfPiece != 0) { - textString = """ - $textString - Total Units: ${String.format("%.2f", totalUnits)} - """.trimIndent() + stringBuilder.append("Total Units: ").append(totalUnits).append("\n") } if (totalPay != 0f) { - textString = """ - $textString - Total Pay: ${"$"}${String.format("%.2f", totalPay)} - """.trimIndent() + stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n") } - return textString + return stringBuilder.toString() } fun refreshLiveData() { @@ -458,11 +521,12 @@ class MainViewModel( type: String? ) { repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + onSuccess(Success("Filter(s) successfully applied")) } fun getFiltrationDetails(): FilterStore { val prefs = repository.retrieveFilteringDetailsInPrefs() - mFilterStore = FilterStore( + mFilterStore = FilterStore( prefs[DESCRIPTION], prefs[TIME_IN], prefs[TIME_OUT], @@ -473,11 +537,89 @@ class MainViewModel( fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { try { - return calculateDuration(mTimeIn,mTimeOut,mBreaks) - }catch (e: IOException) { + return calculateDuration(mTimeIn, mTimeOut, mBreaks) + } catch (e: IOException) { onError(e) } return null } + @RequiresPermission(WRITE_EXTERNAL_STORAGE) + fun createExcelSheet(file: File): File? { + val wbSettings = WorkbookSettings().apply { + locale = Locale("en", "EN") + } + + try { + val workbook: WritableWorkbook = Workbook.createWorkbook(file, wbSettings) + val sheet = workbook.createSheet("Shifts", 0) + // Write column headers + val headers = listOf( + Label(0, 0, _ID), + Label(1, 0, COLUMN_SHIFT_TYPE), + Label(2, 0, COLUMN_SHIFT_DESCRIPTION), + Label(3, 0, COLUMN_SHIFT_DATE), + Label(4, 0, COLUMN_SHIFT_TIME_IN), + Label(5, 0, COLUMN_SHIFT_TIME_OUT), + Label(6, 0, "$COLUMN_SHIFT_BREAK (in mins)"), + Label(7, 0, COLUMN_SHIFT_DURATION), + Label(8, 0, COLUMN_SHIFT_UNIT), + Label(9, 0, COLUMN_SHIFT_PAYRATE), + Label(10, 0, COLUMN_SHIFT_TOTALPAY) + ) + // table content + val data = shiftLiveData.value + if (data.isNullOrEmpty()) { + onError("No data to parse into excel file") + return null + } + var currentRow = 0 + val cells = data.mapIndexed { index, shift -> + currentRow += 1 + listOf( + Label(0, currentRow, shift.id.toString()), + Label(1, currentRow, shift.type), + Label(2, currentRow, shift.description), + Label(3, currentRow, shift.date), + Label(4, currentRow, shift.timeIn), + Label(5, currentRow, shift.timeOut), + Label(6, currentRow, shift.breakMins.toString()), + Label(7, currentRow, shift.duration.toString()), + Label(8, currentRow, shift.units.toString()), + Label(9, currentRow, shift.rateOfPay.toString()), + Label(10, currentRow, shift.totalPay.toString()) + ) + }.flatten() + + currentRow += 1 + val footer = listOf( + Label(0, currentRow, "Total:"), + Label(7, currentRow, data.sumOf { it.duration.toDouble() }.toString()), + Label(8, currentRow, data.sumOf { it.units.toDouble() }.toString()), + Label(10, currentRow, data.sumOf { it.totalPay.toDouble() }.toString()) + ) + val content = listOf(headers, cells, footer).flatten() + + // Write content to sheet + try { + content.forEach { c -> sheet.addCell(c) } + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } + + workbook.write() + workbook.close() + + return file + } catch (e: IOException) { + e.printStackTrace() + onError("Failed to generate excel sheet of shifts") + } + return null + } + } \ No newline at end of file diff --git a/app/src/main/res/xml/provider_path.xml b/app/src/main/res/xml/provider_path.xml new file mode 100644 index 0000000..4495c28 --- /dev/null +++ b/app/src/main/res/xml/provider_path.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 5e586ab8be3abce2ecae8055910c9e3a80925a26 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sun, 27 Aug 2023 23:54:01 +0100 Subject: [PATCH 07/15] - Shift list exportation completed - fixed filter and sort for exportation --- .idea/kotlinc.xml | 2 +- .../h_mal/farmr/base/BaseRecyclerAdapter.kt | 5 -- .../appttude/h_mal/farmr/model/Sortable.kt | 6 +- .../h_mal/farmr/ui/FilterDataFragment.kt | 4 +- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 18 +++++ .../h_mal/farmr/viewmodel/MainViewModel.kt | 6 +- app/src/main/res/drawable/farm.jpg | Bin 92343 -> 0 bytes .../main/res/drawable/ic_info_black_24dp.xml | 9 --- .../res/drawable/ic_launcher_background.xml | 74 ------------------ .../ic_launcher_release_background.xml | 74 ------------------ .../drawable/ic_notifications_black_24dp.xml | 9 --- .../main/res/drawable/ic_sync_black_24dp.xml | 9 --- app/src/main/res/drawable/img_i_full.png | Bin 1370 -> 0 bytes app/src/main/res/layout/fragment_home.xml | 13 --- app/src/main/res/layout/fragment_main.xml | 5 ++ app/src/main/res/values/strings.xml | 7 -- build.gradle | 2 +- 17 files changed, 36 insertions(+), 207 deletions(-) delete mode 100644 app/src/main/res/drawable/farm.jpg delete mode 100644 app/src/main/res/drawable/ic_info_black_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 app/src/main/res/drawable/ic_launcher_release_background.xml delete mode 100644 app/src/main/res/drawable/ic_notifications_black_24dp.xml delete mode 100644 app/src/main/res/drawable/ic_sync_black_24dp.xml delete mode 100644 app/src/main/res/drawable/img_i_full.png delete mode 100644 app/src/main/res/layout/fragment_home.xml diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d99..b1077fb 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt index ab62a76..fd34388 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt @@ -13,11 +13,6 @@ open class BaseRecyclerAdapter( ): RecyclerView.Adapter() { var list: List? = null - fun updateData(newList: List) { - list = newList - notifyDataSetChanged() - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return if (list.isNullOrEmpty()) { val emptyViewHolder = parent.generateView(emptyViewId) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt index 6a0c9a1..f027a92 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -7,5 +7,9 @@ enum class Sortable(val label: String) { DESCRIPTION("Description"), DURATION("Added"), UNITS("Duration"), RATEOFPAY("Rate of pay"), - TOTALPAY("Total Pay") + TOTALPAY("Total Pay"); + + companion object { + val entries = Sortable.values() + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt index 6611c31..745fb68 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -74,8 +74,8 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ id: Long ) { type = when (position) { - 1 -> ShiftType.HOURLY.toString() - 2 -> ShiftType.PIECE.toString() + 1 -> ShiftType.HOURLY.type + 2 -> ShiftType.PIECE.type else -> return } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index f0bfaf8..f083778 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -14,14 +14,19 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.FileProvider import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.viewmodel.MainViewModel import com.google.android.material.floatingactionbutton.FloatingActionButton import java.io.File @@ -30,6 +35,7 @@ import kotlin.system.exitProcess class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { private lateinit var productListView: RecyclerView + private lateinit var emptyView: View private lateinit var mAdapter: ShiftListAdapter override fun onCreate(savedInstanceState: Bundle?) { @@ -47,6 +53,15 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } productListView = view.findViewById(R.id.list_item_view) productListView.adapter = mAdapter + emptyView = view.findViewById(R.id.empty_view) + + mAdapter.registerAdapterDataObserver(object : AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + if (mAdapter.itemCount == 0) emptyView.show() + else emptyView.hide() + } + }) view.findViewById(R.id.fab1).setOnClickListener { navigateToFragment(FragmentAddItem(), name = "additem") @@ -65,6 +80,9 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr @Suppress("UNCHECKED_CAST") mAdapter.submitList(data as List) } + if (data is Success) { + displayToast(data.successMessage) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index 5c79752..bec425b 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -522,6 +522,7 @@ class MainViewModel( ) { repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) onSuccess(Success("Filter(s) successfully applied")) + refreshLiveData() } fun getFiltrationDetails(): FilterStore { @@ -568,11 +569,12 @@ class MainViewModel( Label(10, 0, COLUMN_SHIFT_TOTALPAY) ) // table content - val data = shiftLiveData.value - if (data.isNullOrEmpty()) { + if (shiftLiveData.value.isNullOrEmpty()) { onError("No data to parse into excel file") return null } + val sortAndOrder = getSortAndOrder() + val data = shiftLiveData.value!!.applyFilters().sortList(sortAndOrder.first, sortAndOrder.second) var currentRow = 0 val cells = data.mapIndexed { index, shift -> currentRow += 1 diff --git a/app/src/main/res/drawable/farm.jpg b/app/src/main/res/drawable/farm.jpg deleted file mode 100644 index 290da71ee48cb2e02bdc113647ee79fe2044fe2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92343 zcmeFaXH-*N*Df4-Rf+`Zy(I_&Vx;#Pqy-2_ktPaAM7k&)MFi=AARs+J=wRrIARtYp zD-bMHMZp4!BJHgB-0$<8@s9K3_4{$g_{JduZuXvgm1|zJtd%hL!Sum92rJUS*Z@L7 zK>=|A{~!nRl1uv6ye>i@rlt@$1OlOl&_b9Xl;B7L{NhHz4xs|a6yP6(LjK?9<0%yW z9CHAf+S^_b>OV3N`X?XxAavjyGx!}1fv_3yeFR7J;QR1(h`=ENhX@=ZaEQPm0{>4U zVC!||f^SHGkcFpjps$er1+PF~e<67}SvgsGIXQWGS$V`MB}K^pOiDpcUP(pn4EPd~ zS5#3{R#7+wfzYN>{a04P#+OR{?_;_2v_HpGa&izzdisBk3n~7S-TWtWVPGYz=P+~m zf0Cq!lRiY?5P?Gk4iPv+;NJ-Rw`@a6MNtm&e>w+lprxStC#RqVJ0?638gLHmov>jk zc>jHzMlL3APXw6t`zH1u?I zbPNpijLaNIn3f5n_k8{#791 zb{vS-)53zsT)>T`6wOIQNOlQ_9Ti817c*kqExQLJF-tk$oPnVf1L8$YWPuJ(lGeWu zXlD`QaHE!04SWVH0 z!%+UK%M1+FpF#UJkw1q*OE5y@Ig;suV0pM!IlKXH4xBZ#kpBc*oc*%qHg5&~3*w+BEU}Wom zP^1Ufvj2bt+7vp1?eOICDV+hZWndt@K$?LJWG)P0K}~`MPsKZ7F{PAXtPt5DgcW4Q zVCXEB2QLiLAvI$J4x}J6#*LsSOc^Dnl%yYpK>3s^gkwA4y}Yns3jn1D`95%vguTi8 z&n--GLkJX?Pl<=zG0;*}uG9=@QewnA^@I<8##(5`5T|u?YJ>_$dp1WDFNlle^}2AKoel zGUVH>9=6L0hqbG<+fw}b2>}ADb5z46!{t|5`}>4aW)q`2otp%ln~wkK=EI*6Bi{)) zN#gXo%umjO_A{TFvEOejTRc9D-&l1#@n?jVSL-{S$ag_+k-KcQQ+w=fE}QH+tyZa4 z=N$r#9o{M#E}nM*7avuu@ZZ{aL;z!8qQk8lgE?KT&;Qe>bW{mPG5&!G3j`Hy;TI~=C2-KgSmd`kkc}R2{%B}Ij zz^S#+`fqWiez#J-#veeEj1M3T5f#}dQq#V0A3%8Ke@`aAbxES%^%F3>=lM1yPc!l> z;qkmOan{dQ_J%(^3l^lO!kn(jd#>g8fVgwn5j%6P)w`jmPENtIB>0U3FZ^jQ^M!co z;$Ezo`Ef7Pm$p_PET+QUsCT$IZF^@;nD=U~hoCg^$oBoL9+54R;zXdTIK%x`syaX1O+#F}3gR9}Zzy)C{w(3ip03E24A&@v#XgGnb~g;cZ6Kt9JnDgj=eq<8{Bg8p`=M zkg#`S00`0%57pv#%&xxHIs-2TP z>Q9LAJ}EnI5cYIXGEU2LD?VaqM&bv4Q$(%%1k1)t;VII%#*y$%P3@br(jkDOn2PSW{i z#UBz-@SSt3q@fhE)R`QmNbnUC)0(#26hU9RP|uA9^ZC;j|fg~JBRi7yZ8JL%);us zMc+Q+>w~B3G@rdTTdpxl`M94|`IC8e(|P0DpT9E1*Ij{ixSiLn=U*l88bas%OSzXx zbQN8AHvI>xJ%Xmh?IcdPkelPT2XtxcW}Y@?a<_AolhhvyS{4gY3`ME>8K%mo(#;#n zP1!YsMZZiDbHBSMe*k$^{>(nd#kK3hwbG5&7ccV55^wcxkkl%idnXuw=jTsu#|%cN z_%FJ+e$YQN;QU$l01}pWAZ#o-AYGoA!0vMV=gp(t0q5-AZ7DPj_BLSVJkGb$ zz0Bpkz-~5nzeKV6*`vJMPlx6R1N@D=PU&)SWl+DxJN1 zQwv|$B}us7mg+HEZqC;|DsDU%h78|9sVNVx>>5fH#ZkB&xx zg+0O!!Q;IAFL4nICJK24pBJ~Sr%^pmbvt#8Y1)*PGB!UU#XLqnZ!qkCRjk(g+G5Y5 zaE1HAlj!{0zfscCa^eGA-2>f%i&mGpOqj$5)kg9lb4OK@y`H^SI?C|WjP5nwUD+ai z(sZX}j-D@ky{-JDP`V+1Xcyj1nLqY9H8ZR6vLW~Pl4nEibe=!)ZlU(N*#U#Xj~Ghd zE4z=Jy%gm}Idh*%X&(zw8P_j-0e{4?>hs!7^s z*qoV$7F%vf(JlemVp?I|P;!-2bBp3OFK?Z@Nyp?PX_3r6mi@QpmfGF#)|7uh5l3?h z2Otm*imvYR&pv(+S+?vm5+&1Fw3A*$giuX*;^*cTv9vDX^)rC*W423;z35;4`Xy&A;zr~`=L3e53D zl%DobCgGG6*PL1S>Skkjp3yhCkI%ouj|g2947EOhbQ$e8MU1JB2@IKB)V$XJde5kk z`o-0*ecH^$Q68L)pBbGh=^OR<-L#&%#s1C8?F{@@J>HoQzE7t(Uh_sllM(&-_J`g9 z!8d})Gwhl#&y0>Y3OtH?KPOmGI;$?SySmZf1bvwGeWXS*D;#d`S=D!E8TOt^V=_i! z|9G0;hbiysH1-m17AyaUcLoLSHVI{&uUT%1u!WunV}d6-$RFu>zpvgcxb{fNu}-W{ ziE8YWc~)LlOh6UelaK6%BPka_EHkR(jbu;x*-8k!p-|%VLwDBNFZggDRnJlAhX4{$_2auEl$d8JLT=t%Y zqL8rao;i5}1(un?0R)ts$3Ab;C!dd|Xg@K&?R;Wqy*xB8fK_rYtN8o!k~mM=%p7%E ze#5}yI4`Hd0p~Mxo!T6aa(rLAZ#9~{(anU$Gk!GRnd~$coDji3F&aPZ(l@;r<%MX$ z8$89Gy)2S{F7yYVRamY6gR`v%kjt7cDo@*=%H$fLd{ILAUinkIoT=ui7<&1FXAzm@ zO^L6$cNh%k-5;)E16TvzUQYRhV!EZY_pGX4?NUw_Vc4}CeZta@n>u|s`B@q6d3>_i z-F?OT0(~b8H-)E}&4&x{!WJyb+!a^T6>VI8-?3+laYRIB|&TZu_tjBvXx7Qr!ggJLjEK;zn6T)_$P z`nHeSpX%oWJZDG>1>>c?c3eU4%UJAj21cz5B0njkC|)pd1DE5fPJcQh(O`_g?bCDl zZe)_dm?$3+X-=?5n))c~oA@isl%%?Q>E<)?S?MX9WD0`sUA^r2@^biD4;`7;o=jVl zVt0yz`oGm3Kx#{mIsWi0c2lMOv^}Mgn)RW7dNQ%y#aHg?*7!++a}I9Z@3#ryj|HLo z2M+8+MFn(7cyLjI1Ov|m#T`9i_SdQ;>;Q=IHE3%`1 zqtZsK{a(ApjiQKWO1J{Z&XKIp}ZSUdMj>vx;)L6 zcIE)`22ER)!+t6c-amhAeJs%5U36zPAxpVtj@=_A#P@m|D)Ib>%+P+n{!1ES6Z^(C^H?WE32z3k;%+yVED6x@xlyHQneLj^(8^huu`r)$C6gK#x ze+l=MD2G)4^v1o=^0n5d)D?!UHE3bYJKSogoSC4@*@jI;G`GLiDNc3B*L|^$G3Q1} z8&w7v9I+{HI5S)5!l<-5;LUl#cj3HB-nVlq$0uKM;e4-Uo|DhZzkV@azW@2FWPJ|+ z-Pz0fKg2Gii`ZbRj`q4`_OBZndHzM%59kwjUySIA1!gi=H1`5wxcJ{c@~scVIc| zxI_5n{ERJo9ude+>Q1>TlFLwJ_eDPGfh_E8;A z#(s|FbBp4!;f>`<`(Cj>cvMC1w|tE#+H(4Zb&1UA(+8=1eNt5XT#>hwHC|&fcuJOB z4j>s0mEXL_mAzd(MtyK0K7A{3I_6f!oTYbqj(v zXsei2>!kLy)Oc7O@d(85d31`YUr3767X4(l_#okZCd@FP_H>a<_L{&$%PjuO2v6Qe zS|$GDT&zT&k?XR;2vq2$@%w|PVi#*e%i{*#L)~Fx&Nxzu+i@A_8^enE09W$1Tl zm8*?Me}v7^l)K*@=X$tUb9&KO^QB13q?KvS&2jH%wZAddu{Wi{ttT%b1-tz}{ot-y zypxW;o;UvFsRKoQhr!0 zdXwNiy~fn?&bftJ%W|m??$1&AeIw2ND^OdVsION;%Vl~m+>I`9&v}0hBb!{|_WkFb z2=fzo1=3ym_yZ6MM9f=l;pjAJmt|JIdqs zt&+J4R!)?TDbL5<2tc`9ijmIjt9el|XzZy(Y3}?Ix3UIDa-5dc{-o8%_4HIigi@c- zao_g_6Iwl2Xr#CbX-YVi)y&^X|CQ{WZP#?|4#)Ld?d>8S&M_2pifHEfeDfAM^{Wq1C=V&J8taZhjg z21EueYkP_JEZ$=3`knuzeeB&4nYV9PbIwPw1Ad2RvJ`dqT(( zPd?*VWW%j!?BRJVFeIU8r`Nr}HD}F6taMc*=tp|YW>Rhdu}OD8nXlxUP^rJ9SqaQP6}Nqt(&wS_G7-ioEDKe3_o>e zg6C`6nWBE~PiEZ} z1~{F$y(_6vhQ5(BX8-$VWcja`(8@}`4rRa7nNrS#RfGzr_C93~hP^mNd+_(v!)Rws zR7YOzNPb90hsOD=+0_!Qkriq#n-OFg+HT|gseY$n=O70CD$&VIn3gAwrk*iI^X7L# z#JF|XZRYFgN;M}g`9HIdhZozio1?#1@nWtN3@6!e$u9ISSC{6UHb^e^oAOo&mmW?! zcTwk=(bF4~&Ms}%bBMGbEyxgIB>@Kz_t<*h=q^-tP-j-2kO+O;HqZtrN!L*@w49LK7n&DP2{ zFUxX;U-+3WJe+=zcQ6A!bjL0kWt@#O*Dt#(Uy#() zIKrrQP30QqD#jzwRp=VV=SqOeHFc3ct*d}z^68Tz8fyL*E~=pP4E~V-zp0D-GnSB$ z5ZRDZvcCS8PRc7QE1#58IH{l@10-YuLazk6UX!^JAo@=OdL9Ap{$5uDy?n0-ksEY% z^9>4A7YPdTx}b8=_2Ox_3l~nyxVfIXAR{k-!Bxif!YO%~3#U$9ICENFN#UZro5+6+ z5Oek4-Txn<0R4d}sp$H9xPp%-So`|=X#9^pk)U?+zu)$sc0BZ413fgzZy3nPodI9g zwolUm@^U!iVfSgzK*m_$%Q#q>_E8#g}sA-fLY`YQzfO8Agnhj9HB0)HiZ z$gV@U{tAJ=5trt4Ua3}&on~7-(&o#4MasrN&W&aYVbuv zPp+~>N;zKed6V zz&+G73^X(h97mXraQyY_Pi-K|uav@66e5SUfgnx)zpo9n&3y6z^5AU^yarU^DEMSB zyNBJ~e;2vs{H@4vdq0r4;=H+keN^!&g|>aFj4*d$i)+npphDzZCUa?%mR|>uy&&di z$qD{7y!uADJo;xmjGiPlA;eq+oLBce`}e+?Q*l7?mv23-981~Uj~uc0A2rH`dUF5~>X+$5KfSBw! z!h6Jm4O22q0JVd_poXHj8#6o!%{!JYvoQl*r8Wc`FT|+Lz}6@lKcmgTsoNaY1haR- z9$gams)5?s;6JQ|x2F-v&>Y3O_~ zDl5mR(}bA4GG=5e;!B1W2_^vQ`{)k@qt;JLju?!9lZ+=M2D+Y0;`V>;ZCd%@Meaw% z%TT~gANQNj=;*OdMD-arV{14mg@{Go_NK(!{_;E)B7|UzDy_KCK!u49vkK{}uMx}V+ zy(4CU2CQpd7Br_v@f4?_OG&yG)DE5`l%*CMsFH-bg+Gs7Q`8}7BQrdhi8l=!v3O%p z>F5aCSaSiAGbVm(tmS8r#4MwQqfzrj%)~&U3=Lc31molqN@0}7MT&%4FX}&lG-)wY zW(*Uo0b2Cjunu@PN3Pd&+<;M^BL1v_7lJWZEae%{;gTf^Q=CmVasZ(!m;If}sAoTE z%VW73yp278$R=TrE=qy&2Eq7QfCVrAz*b}dQVaW)_|YiqslxtetcgIKN-{o6d$S0gPopFbF`$*Ag2G4Hqqu0Ot#iXG zX0+JYOq%oAal?i5T>!NfKt_l`6*xsBg@hy|;4m_rlrm*0viYd{%|(SGq8vrurr3lg zp;LgZT3EakC9OTGNp@C>lE*PJ0#7m}S-A)omq}~emqsOmVpBUuq-CtM42VAEYl<9r z7ZHzg3@A;6*g;Xkotgmaoni_-6EP5LycC1Bo{1UET-so^>Ik4X#@I~ytgLP`HX(y- z9gHg?B%?=gOy?wKtI)AmAJ%De7M+3NJQgBKGSW91Hf~O&PXV>J;GhOoA^k(pgVnU9 zb!x4CGoYqaeLspK<@02;*G^=T=DYJv7yhUjSdcVwY?K;FEOjd-AvrU2d|Xm~ts)}x z;ZDR{n3#M55wk_G@sytec57pg6_>k~rM1|6cD}={hong02v2odVoW!%kiL1mf}n->22#TI+2TU>kVLOS=E)){ zm=+rrU%p<*2=S1e&A?Dw6RFQTF~b4#tEmHXkC*`JRa3iIA@?`%OeW1aTH4xBDJ0`m z^f4K->)`dIlp$6(32}7jL$uB1C!+z6M_ECoF+hC5waHRyYiqnGOh3WrZVk0tDS-Ec zf;#bTlkAWMyiF@EDiblQg&iNIC(sjC@m^xoVissTY2CPf%Fb3)Bxl%a98;PZr%yy- z2s(hLy>oZ#=1NKHsQOaUS-n83tuZYo9Dd`Y7OYze`d!!tKLdEjq7v!}*8#bnj8&#PQN1eU{Vshd^g|jLKZ$sloJlVl+7RoRjU2^rD0R_z% ztC+9MLcR5oPQ|Hg>%gx;L76Hpw#>pBVKfZQkA(fTN2fIs9YlNfk1 zz6RV_Zy7%q1K9l7ITn!;xtl}5Gj#zP;w1& z+pENj&}`WlJ$D;J0k;pL6lT)4&%khS#5dgywPJiNY%go`p0sItf^D-peLzm)X|w)&%Cjbz-$Z zsXk1tJzGXXGZeVi14v~eLt3=K$YU#-QYPS5gW0**lQLz8Ect=aVw+${ZGM!#h!FN& zlQ4(^Yv*WZ{p)t1!uE&+7lRla#-uTcNWs}-0u)8ka=eAcc_6ER8Q3zVu5SVUtSfST zJfVBxhLrqdhbKutfr~x2xr{QYbMj78{KIxe>r#thZL#xCO5&QnO~OzFk0{V*W)`@f zM4vFeR(=bF!1$VzI1mhrqeT8(ORZ6*U_d&{fxwF?y7)^ zI?I-VUQSS}!WgZ0ytt+6+2G?GgiI;G$#85D98;V#Y}TbWsBhYw!O~G6!JGG z#gsCbN!#B^O+yl_d`e+j)lcbH@o;Y)De<&5JOheCpHhTmW&>11k-*r%W$#4XBd%ki z{_5Bc8cht0gNDqoG}Ns^8LD>jH1l&Cqt3?52soYPj3P&T#N-YPF)Gb^v)p}mAE?~m ztvADU)^&~|xH~^D4TT6`8yrJiZ^}hWEK!5Z;ua|H2TYpGS!f~sIVlFvbOLRBGhi?{ zQ@N~2?j#MJhXQk$)dI2B6uWKQG*AJvqAoPFR%Kl;LIVJ4q-oNH3uTbQ21AY|!qP|@ zlg1QToYXyTq$d}FfS9~BkO16Pdw5q4C z(m#t%YHEtV&bo#NTHY$B-uZ6FMnKiad;B!I!!T1Cn;QWv- z2J9IgCe2Y~QYnC@&UwNV*s)MTGstI*&{1t*pk$1RG;6wKmxa`)dhE}Mqy#1*t)=-# zc^2{pWBSnw>evZ>bK^!bCWKkGvX`Ut7m)(vFqlPiA046&0?lJr^idxUA>B7{^=z zWk{&B1gK{{D{w?G6bNQUp0y~Phk5-trrc%v_He1)$8eDB)W2L}#x5h%vTzuY0R-SP zfZ%@25-3nOm4ux_o}dOrD$(RRlM>p^X;~nnp3!myB{02%?M>riuIO>5g8W9niOMGt z!?8!Su$07^ozE0DZ$lECXhTE`8G$py(#IQ`EKhdO@X}BLu7NC|MhZ!-PXv0MF=@`c z<va>GQEwg0ZnbeO`YdFD>ndq#T zG&Xz01gS=fgWb@i8Sr}J10_EwJUZ_cjH+jb84;EzOD!fr2JluEC{Sm-!B&KX97Iu< zV2&^@c@q^T@h}7pxlo-o!MEaX}UVW4Lv3jt?2xdeuVrwLZP8f3znZ}jRdU3<-Y7%y&W~^@QtC$i8 zh2+PY%jGA68POmTfx9{S{kk98z242 ziJw|~Fo+SdL&21kps*;4*x^~v=A#IL04P?<8jaL9X^g@GrI4`z=GESBw(-$i_C9fx zS~a|4<0FR10aifm2y2q(j23I<*z#m0u}F#%YH!hm=rL}N83?u*wj#$$*3iIcxIZyh z3OWE%P3x|UF~sWQ!l3%*MNobQb?Rjp|}wn2neN2 zW;2FTY#7BNZ%ckq0u}^lYQk>2#VP%$TWGBK*@jW}L<-us-f#@IOa+)tI6=qyBNnq& z27)Pqu>d62uxZ8}aCUMkb20h}3z5CiJ0M%@NW-iGDz_z#rLAPIdsgoKc7GEx?choG5|sA`T9 z4~TYSYCjq=&QZv?0}BRtAT2-)_oMmU`4*_63l^FEsQ3@kI$8!a0!}84F+>W1A$@zSjH(Aj4@5(9DPC}&fX!J^JcAZ1 z6whF_9XQ4hQo&Tb89S(dtHqfK7Vaj?qg}Bemjp?RHy5kI=&;qA;@wOkG|7x)l}%8~ zf!q{VszHjDgRUVU5bZnh!nI;u3TsU3#~a5%UP@WMu?i*&e9zE5G+55rf^{o!wxo3} z*7Xt(Bzqobl+Y}UzKRBh*a5J1PWlj@jAwtK>;Pf5GEz1Jj02q%S9Spu^3`FF0Wm`h z1ctyEm0X~usGH&t>wG6(sbydmYVA3>%VYJ{JD}>W z9V5xJir2HaOHG^teihs4bcI?NhOl0S>5NhfI4Psb$TfUf<3Gp)CF_jRL`we*tm3zm z28Jrl0djaWaw3=qa?=pmFu5!>4V$J%Sg@Cas6-BlFS~mz_ z=gmM&VOZ~3B3Sh`kD_8~wd=>XgH#Po87xeH2v2(j?*MZHh7cmcHr6s*He-ZJ1*sp{ zdMSe$H%2?9g$Cw&hlA9fN|?!@#|i^7N!GPO89U$vfq(I2uv#x$E2Iy$Y%HHKn5~!2 z{y?%?FP*VY1!-h&QiC0liWHN4XEm7#|6sps@Es)}vBn6Db^q%YmS8 z6QGtBdSjJ2=z^gUx{#41Ri!FbNaq9C6u%nGIC*n`k{k&cuOjJgr23SS(A0rNQqrha zBnp_MBu-lRPnOYgrYj`HBHT*{9urv)=#tkH-$YwrSj+Dg>RS7srCEq(E z9lweM45L(`^B0s&YElBbIn2O{Fm^Cqaq4DU2II!+n_#Qyj5K}43>pDeK?tQj}1 zVLUL@1PhTWUN0T%dg=HNf37B5NZ$nA1n^V9eW)o_botMLlF+FE&@I$9@a+f-n(I*S z7emy6N5K$M@#kPhsF*9-jTs*1{T5!PO4^klpPps9Nd-QFpOsdS$SAHq|&|6 z5Z%mprAG_;EbwmAg?=Q6`yHM$=(B;L){cx%;O4T`+t$?8)6if`bXYqK@{M2B!*qG# zVf*9&@Gx*csFIyu;o?E&8sW2LD}8PExq%VU0-U@7~jds{sqimeh+vA`nE*@8(< zft-Js;A2z;lK*OK$#AK$g9i-R$etn_GxP@djlty98tUQ}oM8q!*m8MPgxoq92@|T{ zzjnx3wt5Xzu5Q!EVCg?T9YR>T{6V-IiJ0J#=Bi*o2ahd{9+&nh-S{}6tCbM_10D|) zFoHvhR>7Aop}g={IPK)J1)r+r@z5YoLlC1=+Y*++`jzNbXsGKlV+-nHSEe(p#8IN-e#>?aIC0@@e0UM55(?PYm&!{y*%*5S5kXnGH5ScPw z(T@E_qE#BE0#2&w1Hdy>hX&3=kF(hUErRHFHTiy5wHuxRjR3Y_OW8T3*4`+JXG66@ zbu5>s9>b&g`MS_-fTI#BAvmjNG)^GH@H8~3x1M9X%nwVb*$&LNIZjytY@#LHP z)hZ#iZe`gYSB89XT2Q?}uF6+9T^Wcq+xX2Yazv1hw(di^1I;;Mmi z4jz&?^utPBK-rBCU|1YSm*PNF1*nDw(#H-J>H}BOX+sU|1%B|H{5bGa;ofLayuqX4nIhN;YiXnu1!=)+32 z$|kV3ly!k~5%g>>50(s;KLZk}hUhFF=D-Qcx=g^$hoEwMHWZX>hXbU!J^=KQB2})S z?mUek1RFhGffXhX9jzuX6B821#ST@iOpZ%BU#dspP@T zWBDOAY~bp7;AJU&D%^n&986A;jn3=^9)c)RC=Rib8x;nfgtgQ8*JT*FRYf>SW@_tO zLTTY`OCT|cDN0Cz*U+!(1yWcI;WxYolffP;V+_zVQ3TI6N}~x+f!A`acG^gE5y;8R zGet6+>Tsqrw#>jg<-5*RqEhkp@}utM&n(S{i)%v_Q7d90IFOP)=`+kkE>G8r0AD$h zX(U>_;2Ap%cuXBQ&oAr7*y(hV>^UrneM45oc3=S1tq%B=ns_jvwrsl^P!E5q zKmu!sT*|x|7<&5haG{||HX{k90|*?=f1L6$Su+m-NRC}-CgbI)B7Om;QHUSi84b3(xUu7ENOtL-9C9r+KWp+Ag zxHMQB8i`ch0QvNX6*jBEin~EY%afQRd|jw%PZ(gAT6?XFFx#j|m75B4U;u@+A?UnL zB4fBDo3WTjt-WPweA`-?!nz+2>n`A#xg5tc>(J9az*`#1qrp(cUB7ym)4&)`TEi@s*RKU~$WElFrC^po#jc31BTw-#cF27`^ zEwgnDJQr)l212~=aM`xCqcSoMc<^S?!hZd(^5rq~ZlzQ!qbS&taOIamIzBC>1YQCx zCT|i}DC?8|1V~jd=wwtUV7ogg*8edy7F#Wfm!B$v3OLESG&mY+Cs)T4-q^^x5<$48 zw($gjtDyPRFs z(%8~e9oQ%cVC-|2h~{@7KU9D~lMrD8d{n3+*Xrg`a6V(83it;@IjCBWvjjLD8W_Nm znW>gT4Q5x^Z=M$nk6%@*1n7bb>0@@frgBpwvZ?y!dCJHL)01g`IH^2cZ7pxPWb+vK zg#tOd@?}(Z)_J5E?_5W*6OZulAfKDUBQ!%SPXcA$C|Z28hSm-jsAAoEEd z*<5P2$uYHX8V^i;sv9bqX((51z2RAvZ_2S9 z(-(vU`^ua+4@3Dn7#G-HBrnbJ1CFH;PD(OH>{#aU_t5w!ASVjmv5K+Yq=}C~#eejb z#WFf}yU`%8CRd{k>0Pb3uv6oiGDN{K)1)l8EKnFtJAAbpk<;O3a3><`7rDIQn15g55 zPm#-Old;bizzdQJpa=>(GR?`ZiY(&#$K16N0NmAXrSdiv zggQ4BU$RDQHY(VrMu63oKNBG&05WSh2_+}rh_>qo)>syo0>=ScBZnb-kY`#4&?G#C zn@fOzIF1cS0cE`>?BUI#rGT`2cx`?G8(?jWs1=xArEBR%<;z(MY}*|_V1~fUCMd^Z zpC=bsidy}&YMUA|Mn3YwzVqgl#9mcgF8f>#BeeRbQ0CLf@mxK926~Hxq~WNty1LA- z53$UVRb^_2&LI4yDW1sB1Vr$DF!fM!J{cAS}fa>UF{GP&8RdJ zLwU6lZZDmQ3UEU)`Aq>Br0@7qOR%RhNnxJNXK<^q#)w8@RykVl%FvaetPBia_fQ;I z_NgUsf_AysvEmvD%KbxUJYUMR)Z(HN?lFv(Mb)NHVJVTGLNqumwFC%DQrAF@Xt=O z%VBf9ov;cME5~7+LptQWmvPXV3^p3<|8_{N$bu)A(5jFZ)hqn*%)ptNAUVN94UB?! zK>BwYoIQ$aOGuF*FKisUhipN@Xp=<5lw>O-_nn~b7>)osY4BdTM&3_j!53c|H1Fg_ zX_UU|L%flhiX0cgIXw()#hS>jXg+kkE3f=QTX5)p(1MJvb+UEGH<{%p2xw+LnvRdq zbH+kzPr5(!p(ggHMv3fu66Hc(i{_Wex2M0I+!Z_C=%`q?;IO_%vNzqNl_n{vU*8ES zt4r85^xdg==nxiP?Yw*=xpXOQL{eGv&KKj0qmdV!XEr{EcU~+nZTcqDlB{OQp97wI zz<_z$hkLOz9{c!Ju!8ww!uWUu{921it6^f_F_nN$W$e&VEvh@mk|Db(HO^c3Go!bo zE7}V@?szSDe>=jml`!5p;>%f~?XVO{UOy$IL$Y_N7FoeG4XY#cGzl-iVzy1jVv zX07O(oZcH-pEqVDStgX8zIljNe-GXPW+BaqS?UM*ZDvMdVcH`~lKHVIk@f--DVvl< zXU6gy+HpalHn{$`M;#4!rt4(hrr?IqBejT}w=KU2Md#0ZZEgLmIyswNIqvd&5JRUSMXQspA)NQ3ydDY0**cr znb;byD1B{`t*ncY7;>5<2GHCj8Jp22ICi@t#+tKm5{bz@!J{-X;2}1OH56XGWBNT< z@cD*t4#z5Hk`ZSqc!OyHG>9p$p&sXtZ{~rY9XqRR{%onCBjdB)i^N?A2dY#SUli=U z*`t(;zj9=TJ62GqKHW{h2Q_QRb?oR!Ze5=Q#+ZBwxu#F2FX@$48| z+EJNT8z6~BDA;~V*?)X`yQf#9|MiaI3$wR|lVV8-y%)P-9cG^cVq?@(R`b2rlQaDG z2+Jl+VKtjou#wkde#zGkAfH~L!<>s9Lv2|~-iUP|t8-P>xkZH^JdM2Xl=xIbFnvR^ z)uc4MJJTL!;__9F&hp%zo73&JwD&j_bdj%Mu1trBU(52ROfi>`cY>du+mig2JNEK1 z9nDwla<7Cd&^g)`-*@h@<^4IQfZ2}lz`ED*)zQ^`GIw5+PDwj$XZ6c$HRij(&Ffab zhn*M#)81beYZ%<@lib^QtrDqeuU#RQ?lZG_00|1%O}c);UiO~k2xo)9^OF7ZxU?^u zft9~sCv~Qj@70B!Vc@H3vWW|OQ6eUwM2mZ=c4rH%wq^2I*6?k|{_j4$_DbW8(8rpu zNv+4WFLtPFK5e$|d)PRqvfWURPAYzzx5RvX|Mu7FAKz?ePv6#SY`S4IA=5q;!eTIZ z-hAB)%wZQEslzDtDS5q+_lhF3e4uOCybI@#`uR} ziZ90sPUI_GTg|hXc|6b3nznDMF~7L;{79{JIO6X0$%p0{GzRJ~G+%7&2L^#>mZWIj zH~M|E;fv|oPD4kl!1kK-=vtup5%08z&0A=Nz5~eF(JD!S{^SG5IQAM{&$rz>c>6W* zP$94A1ITElM$1Rds@nGjGM~0n8c6$8(FMIhr$xPS?7nX{B)&W(4N68S3w`gZ-FrxZ z56e{?4lh`HjePuS)hwm!_k|k=kOgC3*)NaUSIj3?%709Dq<gTV%sg49bJLK5OrF4C#GwHXEWSP$}1&R(W>Q;8~)ph zE&CJ`Vj1s=&i6!}v(~%k4a48Qf8^P)?}NLUxE7ERX7KGq+8VBxr|bYSzqiR!u8Y$D zCfO@^X#@X_X0ez+S}_$??(Q1UDA`6={o?KUb^sZMovDpn-oEpGx(UHT{VQPqJo|#v z^juB&-4rvrSDQ7OM73_0soz}3u4UMye-n@~yL=~D%8O@ygTWzH&L}US!x)JMrC$)rGyHFo&&3_=40G0sUE0u|=1FayQ=svHG#+Th?;}x$7rB z%PczOn`65hX=#llNgJo%TkQ(I+#Q)4W|a6c?>?})x_SV4Sv}LqcCA?x_x#qxUY5^Q zRn~Io!vL7(_BD?mTn7-?N6xoI+dG)0*0&rX3=#`z&yVWqWkj_KEpmNY{p2vZKY51j z08%rW<@|e5nq`H>C!%ToXD9nh#ny=(-F1ik&z}NM6uyt~e;B;U?>qY) zZ~i${A?sbx3qwh+x_r%s(+;YES@VloZ@+}5J*$q0V!7^<&+|?2t%=d=C1)I__wp-h zN~RkrLrv%In>FgGb*hVm!ygl$-rM}#a)I3etj)o#J|JTv-%RuO zRNYqfPEq8=+Wx87t*Ea{IX4{jezL$Td>;%H8T-7eHGX>i0D^3|{B5xZW!z}n+M;jq z#$Cctd7w*qglZGI5&k%8*VeC5`bKc@#qBK5ySkH0&GOrO(xDLoznVu3?`S?)Y;8rW zKFlpuhU5iZ$?ji9ZHCHLT$VO3(a4WoJahYtSf-4|>)zpepLW$2sGrO-`-+SDj8^69j?!pL@Q$R`waa)gP01V)OX!Mk&3!`{PqzsBA`|ce|R` zmW-L;n=6>1-XBzDw)uvU%QiKN4v$#YJ#${St4({M_0Xajo30OH2j8#e46aOF=ZU=d zWJlmZ_>R^vFwq$`pAUKi8=y%=+B%-r57E5Q_n)eXJby4x!)#CymRgLmm& zQCeaf%3B{*UnRdb-wG9J*`IcP>%nI2*UD;Oq-3=nq>-5zTIqPMzfmmNA%1jstd~A~ z`o+@0otUkTG(h-hWv}oN?aTvU^345)0uZt=p{o2~K z{mjUHHitW632&b#3Gh{JCzm{b*TSX7HnM78{o=C2y#vUlCnkE~%98zi>?R>IAzOw! z_Qk}3$SJm;4_cD7e|&O~5A*&ynf9?oaF%s1z0dsuspB~oK_TqG4N28hTj(@*W`4a_ z;L%&($2t0ZlXheHD%a+yTlL^2=<=h!;>*F0!-hU>v9b)O^+cyD?>tgDHe%|YGfcoX z_NG)HTa=7@OcWvBm>xgJp%6CILleIg+do7VF#Rnf=Yj7x=N^>f`M$k|%U8BOAA5H1 zN!7CUTur{ASh)6j!mpQ?1y4RV?1*#f2oCiN>pgnZSly5GX?0je$oxUO>UQljCWUWz zFVRs4fM=7{WO@HCIl1;@Zs)x7od}+N_2B-<*V`BA@)lcsrcQl}d~)8sT)dF9}QQX zxf} zx3y{xyy`iCd`Px~BF+u=cWk_PDPT^;hVLaI83<-L@a0$*1OUsbS&KB`1sNAQ@_!* z1v`4ba3<-tSquES&z`J5)Stb1Nj2D=j%)Xk7f82I$0xl7?e0l!US2i=X%7o~|K>yc z%e})J5A?d2L+0D}!s)I*W?8qa-1h%6apG;r?=rMY#dW^T&43j1>veTw$GUAa@@z|j z!g?P!36%S8n0Q?}=`?P5ELpt`9=dP%$ouiSxSuco!^YXLj8i9vs=64DhW?KJ#Z$3Z z(OLZazW(TM(IDQc_Z{Z5&g<+Vp_b(Xb`PY41V8^Timp4F&4!Cdc^h4{S}m_x(Q3`2 zwMko>lG?Ss_9m!Jlxk~56E$i?mD;0bVpAiw8dW4l1*wtPNh9c&@Bf_hX)_uk*Q z_wI)OnC)~ALSs&M=Ey=mOI^dUtboh>f#a*PiBnr2Ij3$+K1VlGIzdb{|@Kh z6k{0ge!cvod=|G0TPo}PlXySMF+WJkNew$N%-0r@+DVovqGOgCl1VJ`tN?*`+~t&+ zJzYNRvAT7ukDo-QJ3&yJ%LHq35WydAe%e%W>m=f{H!XFvA!_6@TlgV3PktQI;H|G2 zl&WlLN(98-;-WW?AJz=a+{yoZ|G%ZOyofJ-emttZqqi=i6w^#sq z7MFeO)7~5Xtm>c%m{H0z^o)a37dxUWAdfjDer`!1OG*3%Oq}3gIg#Q$R3~8|H+5%R za4MsJ5q$s5>#dE?0xFKb5cT$`O0909@WspKv80EXRM_bH=solKq7UBhA3)4~6&NA# zBS}?lNPiqfatw9x2quZXdWzIy9b3^_>a{TNb4B_ti588NccUHNFyD@0zFi00S}dKL z)#O{PRVgQrRqWStJ8=0bZU#VBJ?-u%sIyUB*(rSOoyTQX<{y)Uy_=;A9A#dkcm$8O zQom>hein}Q`>^Goyw#*>Q1R7ynXNEJmEGpR)>Q6-aDqhtbf)h@Y38IV);k~vW3(Nn{_UeQLS(x@05QI2AvaNV8bD>+UFu$No zLPa4XKwng!hisnUWX7^BLQaE+U5dl4lMGh!5?**pIc2u&sum$NsBC4DvpDZmB$J|} zI{y=6u3#eEGp{pVylPb=DFF0rPo_R<;A?O$ahiRyWjIfI6)v&UezxiBn*{){Lx1jb zUJv!P4!d>-M!3}aDf%^GSLC3$*8At?L!ajKPp5?tbozF_(;#a)Rc5t?LoWvMwE0we z)Q|CLdfnU?Io6Q6BtI}x{v@UT?ealmoTTzfz1t*sQc|S~cYC;;q}cBtN>My1y9LRF zASW$C7ld7Lyvdf%X2NPu1VYk|3E6M$Rzme`NB>(JNfBGvf2b(%#m<-I zVMssvoo|}8uo+^ zYb)b3n~{4SjACP2=7k+K7ta4%*>0`NXOkI>7&!C!@=fRjm%-5+hd+l;7TEWk?<=;6 zOrWID)ujgiJ>uUvi*D?<>VvMss+@xpa)OtoyG0Elysu7Wyi|r6*t)Q3e8*Q+`DGdL zwhQS<_2Lx?mC?PZ0zMcq=WalRv%H=5x|k+Ih)cO1HCr@6XpU734#E7#T>m1!{ zbfd{x*?aqagasMyZOTAy9L+3^%u=ZKK{7Q%N{0$qRrXb{(JHd9#P$lK5o&K4GIPQ| zUTAog97GV=UH|A(ZhKk_4xb@o?3J_>c>1wL@Twz4RSysznUr>mZ|+p7Wq$Paf9o$=Y@kAt_w@eY9)*uX(GHs z8Kva61b?g9d4lEQ0^6^9X_{PNFp_@(9Okz6w29wKyBTeWozRchq4u&NfMs4cIIP!{ zeTB7c6rP-Lwo*%)zJ9o47a=|lb*r;}6IvMLk{Y`D9vjTGdYra-U&7Z%d{zHl6^;}^ z4NGow_@^IODtPuX`sbCCnjQ9Sy!HX;`}r!5wH@a)$Bus9Ji^V%YV zH=wY_)wEYQ1;+mw!&HzgPoB7W6f0>V#550dC-4tfrSCMoIzwtdyHb%HIak5A#i*a# z`mJx%Eh95rj6sGW_k@vbdi$zZ)GO3!mikgP6NQt&r0aiF(3(yM-hO9di3KlLwlGIm zGAMheh!;{>xH?mUL`-OZ)UieW27xZQxUx7gFBX=oQd@VPt*3yN&`lLcvFw~%hZVNH zLuy2uJ3Ivv-4&M|QlD}BV5?H=UoxsGE~UJv4vI($!5IAKh@%!_&=??p^EGs&DZ<(c zua$ZeUEKMR8lo>0{t_QI)AmeT9Ey+3;j3E`bAHKE1~|pDx8fdFU9KH;PiP%774xn0 zmvc^;9g{^I^DhJt6YPzv@Dx7FJCmf9&}xYzcIiY?Jtl>+?2MG{!Uq1ai1(UjPdvKR z+kvTT$t3ZtxuA{vA6y{RfLH=lRV&OY*O0owVF>V*eX?+_Q>p4>k>i8si<;{?;4Up}>U?``8aZp4JLq#Zc0 zu|KoWF8RZ@^n7a4{%~~PVR#k))B-xDGlZHIh=*>KX!7;-6gE*bY=_q`|MFKlT+KaR z&&|jJO{3lRRa$^il7eRFSU?jb5>slFGPOgguEA;tuT( z!(l<7jWIDM0SBY)ty3#HKa2$=%pjMsUMxl!U#DR=MZWj|5yQU#ZCzMfWyM{J zxIeo(ihcB$J=|e(B`sViG6x+wqIJU=&cw|)iCTHuf)5xj@p`eC(<6i1KB%5s`V&1a zr>%N{N0}XFcr@|*_?h{!k{{;H3L%<6e4xd0e;F}Vvp*V!pZ}+Guzn<}GhB0%D1ypU z+&K04sbtVOuLeO%fV~y3C=CIevb8r;~aYBD+re_y&A?9CSp? z2LIwkr;WlEy zEy6>woo}_LFKquUUu5LM zMlL^tK+{vJzj`)ab5imNf1;2CN!!lsbVuvn7M(ee)*c)XXJrP(wuPLpBmM~~nd_(C zJg%HUh?3o@VwW$YHpBWus|^hVdiw5|SWHn4j~@luJStmG%!5MTGgKPg2~pUGv?83mK52#R2;#cyv&|F2npE-^>6xJgC2>mPaovFc)HC*`vJKXso1iMhuUa{P+q59 zYW{Fv#h+2{oR6?}Vkm=~1X7OL#HF*}t%Gf+yS=2rtuFhK=>Yei(Khy~tz_d4NVYEU z>mV;1IN#I$#ZRcp9gV7i(F(qQ$RPAwgx_^+-lY8S8{S+F#jfh|(}z`HeTb+SpZfE; z{lRXYF(#+@t=#;5v9V0u*0jOX>3iFF5)7j#0$|frin>WwnBcgRdA4|~8PvShb+Vz? z5~cehyka|Vtts`hX9nf>p1NO1>yG8fkPTdK83j~}`_$*Yu+hN_UyxWszjcNMgB6D_ zqp-mYQs=gv!D(h`SiWlJV5{$rl8T!OZ`zc=WRZ{TbNEB;DclaHnPnP$PQjPme!#S) zb5WqKw4;0JFTgcaO*tfKn=Z7_x|U%anCu+6xL!vNf}DrB4O8wFXZTdrDM|`tV3~PZ zT$?98Z}Qs@`i`A>KIOCw(0Ahv2EYS1MbA$*UXX+J))B}V_7YDNScu7i1E(J4MDI^*4-D}MMF&;augeO4!x$^Ubf`5FeP0KP|<$H)i4A^9Vw z-luB{^tSz|vJ`&=gS9n>kWa}Ev}^p%+KnrZ>umo5J{(#Lu!ROJ%4bM8ZaGPgi?}l@ zfCR6Ni-2RQAZ+u0qIDlBM;YFiy!(cOLcKcDCCDY*0{_IEd!;09G)xaKa&ly8+u`C8 z-G1t^p#&Q@*ZvED6)d9|Cti{F>5ks5{CS*B3D9<-c6Y=VXNK~YP7rvoy6iSnKhktu z{52!LxdEN>X5*t?8MC0Y@G^(&#?QoGRn_?;+94i}cus<)9#U~!aV3|^{udx#eRn@S zbFi`R5w)(T{S0c{h=HH(EGpcFd6xBC)?tpRY zbXcmhIT)RlMSDjC@Uoq5KGO9j{RM>M&iru&{aT?aZm-^J|LXJlGBo(FNRF`KP*v+& zw)3)w%lRWAHwKFs72tHLX6YiQr!3QWkG&oQ^T_Fu}{dx?H%cYXW?=~#^)nn;Zt<)=%AzUoULF2|SC5trD zEw8akDSg;< z>Ss;$W@{FvE-p}w$;S5|*wD5HZODSwNNdY-?i<0hVJ1}6*!_uz#@e?&Z0_9U1KaFF zfWRZQ5BToE^cu-X4D~PIx1yqmpc@>6-B7m$XTa(C>vUYmHC(mg7H+E1U6a>BAaM2N zl2k^eg{TEj5)pw^GQF55C#TWtXJIQ|m zcAizeu)v`VZ8ZJuuo-QZ!aObTWFrxZ&REe7NzbF41f{!S0$dd=ZR%!TE&uC1SZQTOR4E~&W&FDk1DvpfEsP|M&|2Ut+7A~W;hj*)&z zpWFJdRHnE=x>+i)bU^UGnU*_h^OT@ZQs=O?F7q@ zl&}3j#!CgFRCSpX^WBHSlD8$=%qhWCcZAf=#}JtshpKqsQkVjTZ$mN&2O(W~mn1pS z*hJ>pWf%GW7ofr({)O+T7Z!cLgR*Wm#y(_-XTn^5#Yhqp@)syUK{$GoK&!)fLv5CO zs{yboFVdxwtby9VUvtjrI*mkyj>F0-si#kqo%5dq2|K2e4eYi3`+oudHZhJQBc@}u z6u1OROVrD8c%6M(LtgcoD}goX7)esF#<=(jQ zcn4<6-MYE&9a^M)wk5hZCCFHUp!asm5Ja^UnDeVc%PA!;DnlR8-+SNu0`0d|b+)~E@nwM_z+4S- z;XRz~*%O)nG)|si6ru$%9yIkmB602wYZaMe2{tz8gVU0E%r#!DL)=R2l$rO4;P}P4 z=;G>Pj{dOZcI?{BPwUBl;eu-=jS=95FVI9Ci^C!`dE-|#1 zarsfWsj0@o_w5%GOoW1qK>otXO6)KFP!e?*9hqf{Yb_9b(;uFeKM-7)$0KeahgPg- z;Mc;o4%3L6W(F1hGBOu<-HMq`?+?4JZTr>N7v86}{Td69#bQoX#q73p8tQ5M3)S*I z3ca%YYjdpbFptF)iMYHh2=yB}}B zS|aWvU7R`tU%i@jKs9>m>%!_OQ7G;Gg}h=jS|Ud|@c$vTWIEyg7to%*NPvkQ$Qw(y zs_gq~6HP|m8FzfSF?IcqkRN>mR#sx`5pCN)UO$rBAXA|*#>x`?I@%F6Q3Nd+7a66( zz<&Kj%{tY>e*vjU2_}vwlg=DCix8(S;hOJk9}BZu5nnpVOQ-y9zbjh20kUb`|A*g5 zFAFryZlbeoXsjzYTGbzB1;5uZ0s0FllIx?7%z;=Ug19QE@9(rldu?8kz>ie7)&;!o zc#->Zz^*;O@qpq(Q30>RjuetgpmRTOu>`*?WfvRs~C|^{jzzwkFx&9q= zv&p*hUt0S1eay@VWRe2Q5q2~5i~pJ*^z)2;LuBUF-feoO=Y8&R06_IP>YkT8Ty-O) zL+VIx_!{pjZ@_tF#$zUC7Z;;O2hrq{r9hQE)Cc|Iwg8`yGuSUkmZvD?yQPb+d=WYj zJZ9GM+XgQ0;08KTD6cp0gYy48-IQCOAA*PgR)c;6!G=fjb1P@-)DxIq^tjZjPUhN~ zBs+0jPcb3=9%u1QD$j(3<&P|qc>+VzP{7(utu(=;9IdqLcOr+f2ABnUwqeqL1}4z> zAivgjix}zHkaOY57b92;>C`LxBtMB4RBnYsL8pU~Q4rXPpP|IvFV(JF5sS+I*4pGi z;2!#Cupca1bDB`B-99-9Zf*P+*}tiJDIn@EKq+t_Ur5ON*|h_Co3!y-#?L-PRTE%4 zmVW_Bp1EvUXJ(P){(x^!Z7(kE!Z+*>ulS9@&Lk7_>5_hA@ad7Jq#NGexMbGna}-MT zFQ6Hc?>7`9$c#Fd76;E`KVKaCYqbp`HoS*#M))LdE^7gmKg2F4cFcX#_{orH}(6xt3>VN6eA6uUNce}^9)aq1ao@=$20 z&2c_1Ko)rx4h1d(+9Z*If*7rk*vkXUH%A9>0N3iPX@FLAQMf6ksj0($oL=NQzgcP6 z%QYX9mb?#Uj3tOTV8Wc*bn-n#Mr%K6>9DxPgbENw)qIdTDRgY23hBk%D+O-64hSAb zZ0>t}H*5sWvF!dz@oIjd6niAZA^S=ZScjChCL8rqLLkuot zW^a%Pi+hr%#Oa4wg3h~;r?A$g^PezF(9TiFg7>Ahfo9>fK`s$y#Vry_^or~dT<7zF z#VE}zoOdljIQD_oWAHCy1OE*AqIG=G*?n&GgKlOKUU(oachd8l4_iYc0|&-+<_HW` zaJ-}eh%9NbhF}w8-5`{+4zjA1gl|o|$SR^JQc7U(U$Ha42=&^K`fZ~3)Pqa20Ig6i z(3C)mzKWV{YgECUfV=I;5R>ZI{UhMY?kH^to|Qq)8+N!w?RZ%@~C`JS){U{4O7 zcSo#kqr-emMqKc2oDnzkj#N`QGn$k67WPRu7R2%?LHWff6(d`}- zw6rc%ayV*ZzlpT)4aLg5fxy~@l?t^&BveWeau_X_G0?`Fi`vbkt;g9KR4a>l`QANk zF_O6s&g)#*IQOcXK*Y4Ey-VWQ;*wiNPG*Ewt<&&$^?9a~7W!0m(J66mf)F)QC0eOb z5fdK72V?hCo6c{sU%HFe8z~RwVCB(PxCq$V8R)b6SL1q{`I)Ck#>UzOOJ{nkzwV$& zG4Qvi<--P~226}-ak+Aqdn4ERwlK(s;IzIj0uQ{HmE$G+Fjb+p39#=sXa_y*A z;jpDP%AY$(m7^ez*2jZfz}SF@pgyxV8+8$0JfW+n*yPyf9hdvA)<+zH_VIt?X>9b>acpy0Ij4Uuh({(!b}!63=H0%ul*8s&9z7KmgHp!aoJ#Ko`K@z{BO`}#W3rnMB)4EiT% z9HQ%R9N1_%V)PfliwH}cBrlF2R^yrA3R^of$e*y2Nb7~DYN?M+yIWXh#DN>==mxF3 z$;3x%Wl>WLQeV7VcEFrBas#0KBzZOeXJYMX6ZIC);)>7iSD#5SIq4WhP#U6(K?x8O zdBIu8qSnP{8m(V>)(QTAzyVe5YH8#4&ddXvJxC)l$7T6k@A7iKZz%7<)2ycfwtB{PAFw37=p8?&kq-U-+~W_0mI znjMOfYo;!)#3jm4H64jl!_#O}T2n1h+ZNY~f8cSSzqN9}-l2zXI1unhH>Stp-&Np$Z%3*# z#5F3U^9I~v*bc)7P$1TnM0Ov!C*s7ZAQ10OZ@1wS+jGM9fVDjJGlc zu(JsK;xANGp_B@vBO;PQ5d=)T-$%4s&-*#%H52SVtwE)jMalZf^@Hr5Bz)LIv6$3YwSH5~b%4+M~N2IMS)@h~eUk!DX&UFqs zR3M+v-t^;5@msI?Fz44&gqj=Rdt!paV68e_U#dSqjewfa$X{}rJfF!fxF7wGxzySO z213eMjI_i2IwUq^I4!x^LIPxDzdQL656i)xiK>qG(A&Z9WN1)nX6|vpsAzjnZz{WA z#}YMTFS6AP@#7vyM%bXy z$oaALd!^;W@eTNq<|F;2h$MZ6tJAlpJttT;2EcM{iusWLNF}?W!}Sp3M&C><{3+tm z&!~HRqiVtK^)K506^}}NhK5^&S@M}aw7<=?xW!z2F0Z-l%_G39Oe9Xa<`Xp}V z%a7*g{U+{2WBrH(i4{mE=pWj9UXES>Tet&!VN`mXL`dPg2e4^xK+42wo+pZK@?64X2G*pVFT5ojLni3t3YG(#n@z96xR z(GIcu5gr$1HOh=#OO0-rv?26pk$_pC%Sgs#xc2fa*uhbT+D&y%+i3HbMARvVZf5n4Nym)Q-dodTJG|`~DGw zEdM2ZCq|q}q$N-DmCrDaJ6w33G{|jCgV1 zZN*GqC$V?jeLPkB1v^-7s5qZrlJp=y+ia^9#cpgVxaW)8h$?KjYtl=`>nw6R+{GTqL(wX`r z^DBn*?cc~^dgnvE5%lF!oi1)19EyVCi8CdLdStcD%Atk!e^8X#NElbDL`p zlfBOsxuKS-Jr_FC{la|z0_Nal;&Z_V@?1|DwT;J=qwv*l=|xOVipP#zRMVhKJL03U z2VcgeAUx}Hpv}S0?~XNI{DKW|$o46paH9F;d{3mSXxIH7UiM7x1HV zz4GWq6UQ7E3z=JCr(gPK)qVikgZD-uzgumYn?nZ0mx82~;(`bL?+q1~1?+T~bJxW-?xgBxh-uI@`+H5@7i#}ydcQc@`iZiY z*xNSEf55PXPp+&ix~EH!?~2>>KVZHbLeqvAM*JHOlrV%;3|ap>rRgCsH?fu)DRm&L zl(LLCxDNkFI{cEhZNVdyL4K%7GdIY;fBOR)kb5D2_-m!A6vIHOy~Np;33FQz@AOP} z(&_(xEmZ|oL|JdDI?Au#yyh3`5>|)o!Tgi*wX(yK7tugJ6m~u`%y+c=*P`}rp?DK? z_(rP{g{0eIJSoYhW9P3#*%=jGvZX=28!UrA(L(B^0nTern=WvJIya1lBW}%KcgytO zXNXu2Q$3S*7S^m z-M?q75QZ}y!#dqx;-FuVSKZbG4>!SfM|%4+Q_mT;^P(kp{7B~IA~_wFWfapOPz`@r zqUi&@lh;uZ(ju1hazWfja`p*`M=!PQyzJ4K;*CuCek3m;WxQanqlC8~(%kX-0*CS$_MHc6>$vZ5CgH#?-k_CFFYF+4mLbB{az1vi z(_S4gcY!1L2`bZpOMUB_9`u3FmKH&iPffSKaWrEW)$&41vnh+?qq>p5tIsSF6}hv9 zF7e7u?Wd*XwTE#NIkQ_7w||v+D%;ohPo9y<5|yO8>tk#(k3l! zD*cGgVB+Z0$6Wrm0K4H4ZhW4N%^9kxYX%Y}^NEpdnJO`l8NCQ0Hk{C8K6f^E2b*&v zgIsR*3pYS&hzU>%S*8q*T`Q3_nyyzkknb22+CSUL%3?hSk53q6Sml541n;kA2>aEA zB@Ppx7D;z=86o1jxALEvzOsX0wy2qZ!nnYpaX6JIK(Rh}hht*F&hk1m{>A-N!N^%b zq9FeQkk=GHIEtve#hB)jBq3^!N8{L&uNpFq87iTOyg`!(j;BA0p7$f7reFMFk_lh~ z0MlkexL1&Y>yf|nH+X$soOxe0416^pm|DW$_>{6Ieo~aN^6-EYC6EW%VOI|25@yDU z?A@?i^&IXY3GD4i9nT5u_z_~X0Y@tNIlKqiS-lACnjXr}%*;#yFB5iYfXEm1uKYF` zgz=h2+l$^>OstAIz0l7F%{^0RtRw=qwH+AWL2DT!Nm^?Ev2q3G!r6aI=kBVW|2%hj z#lAtcucxuay3`qiKm*4W`emo#O8(HaiMoZp{iy0+K74`A7M>@~-O{lan92K{@s>J1 zho@jiVd&jM*8?tT1*hcIy#f9?;JKoOHC&rva_U{u@P2YN&^X);Skl=pmM!8lK)yxd z_cl@+j(6&Kl+|>hXEiR}Dfs{{+o$%^8Bsj&QKgUG`aa=YX*btS+%$Ax=lKVn?G@dS zy9-rorT-Vq;ekv19|^4XXL-%e4$gQv-SpWZ8)Eibd>Ib~HTUABHv zluCr49{F^U_8KpCj7leytbgvzAx)Fhsv!UF%iK8Owbc{r5pS1&IRk(d)PYP3+}A zcJ1MN<1=xWXeg6H(7~Ed7fnY|1Q?fm=ENyn9|mhrrzn-iTNw;8=1yOqT}M#s@u?m! zhSz9@bdHBSRxba%+bqyg{@|R;9{Al!EGv_Tb1XOW)|FNwjeVp!z?3)HIoFLJ)tMY& zx5D_!7jLCQn-sr*J`rw*?*k#~GnM4fBNnvZ;_$G{s+$5>dT@Ca^_Gh_uutiNFCjHt z=%m^HuBJCzKu_H8^%%9^8%vKQpN-Z_A0DQ{jz7@QElp1z`^%M?4ASh^kAS?>`ExRh zHjxNF^rAI2nGn~-1Q~iD`!g*!)e9{>%rJJWRmhQAU)fxL6>G@Ea-Bw5`RKz=c^$&` zZk(g3FEtF;by-G-n09>mUr)mxi(2Cdt}4dILm=Ynsopkm`E4fT+-v#vda-zyQN6IU5gXXptcxGhvD0C=Zq~|ul+88FL@q-fN4@ncdBj&Ue=`1P zC0k3iiYMQ?(_cHhA{87sbcA)mBO}K^gfBA(e$pm z!tdYVhPOlke}`nZIMiGTNvgp79=@?R6XgjjQt>;ejM95=_*ev3u&Hfi)Dw!pwdvBU zu;J?K1&)eu@$PV^+p;^@F?9^LClXO6Sch68C(rA+b`gb-4#x~d)m@R=lpr07_I~lqKjHZwF7F1W zj-_QqXypyJ>5kOyX8IK-SM%+-?vM!eqMe-5x7PzS}Xfrp)0{% z_ONq4^P1Y=H5jodmlvS03f=evwUXU}xuG>^E|@em<82l~T)YUT`z(FbYTj<}E;TA) zh_w8cTlk1;jQ#gxiWdVa!K&z5y(v8;^Z(KTS05N&;O0Md$NUJBEYbF>ib4@c0%E;`O^D%w>T??7UfOI)FFs)f6)Dh;!_TaEonw&w`D3D_U7@ z=6M|GAHzwA9kyK$37+SWNU4pcE{s8H6<67-uHb)enO*Udk)oY7{{?h1=)8o_^(x(e z0S4fmdWK|bWE5y$HSjAbF-@+{At4HVbQ@{d@KIac&O9(xBZUFtRO1nLZuu(hF+|LR<@^ zen6FUL~X>9$*TE#4_czCPM)!WrHwT!J+EQw#NI6^i>aqh3(S0exB*7{y$@WuK66X` zrzs;VyG)&B-&5-f{0@uPJzp!)!Psy<4K7-%C)oYcXu?H(?Gk6pIDc6Zx zIqYFsbpHOH%jHgz7*qcD=erxEhZc=~3+W;bXWx@{+KU*@-Hn{OpCS;^Mf;@pX%)`0Xi; zob94BMewpUV~aoRP%J4V$+58B*HYM3!RA^6fT0>rpIg$~Uvk+Vm)ey|$3_<&Y-rCq zA|VT&H#bg{E#Hl={38-yaZ*T`c0O|6^1G6kt?A+4$_@YXpK&Qr1OX4vAJ2ZT9zhA# z*Kd7?$ntY%sCj#+66I8Td%dE0(+=wv{Yxc_r#O!27{?DDw>l37^T2Zsuqqc(4)|}o zvDb?!GOH7KU0(Cl)uFxWwQPf82VJoA`}evVi{+l-Zj{&V2rG4$(>V#!fN-({QX3=) z-Ijg@@zS3q&{lHX7U@NVEqFe$QA3IhqRH36x){UsDVZixWhTCqXET!>xL?X)E)5N- z+f>%z=nW^MxE_weG^FCrNm8EUJwii+fs87B#yTlJtI5KHM@vh$N8acaB02k0OZ?I> zeFjE8SpeEee>W|89(qPu%J~x6;FZws1QeEb{ag)e^GOfX-Dd+uV zfFn$*)21cHjc3!fjNoe&qs4)3jd#bHC3I<9&23MsZZjG@xGr zsB{^ohj?@eLOV=h1a&aDIJ6w9d$sOQV`JsBFOq6MISYGA=AYJIvb>9NJgE_`-ub|< z_FI+Bqe;&Aw19&wf!wWG8!E545>>jtiNkJ!>rkZ&`%^-MfUpqn-yaqni)*^a0@dzPFb8TGLo>S4c;8glD+W~eQvOE{btrG2i- zvL}KUq3I{4u_bw0^%c3*Z!(|_?s^wE`Gk5fwHuwikn9%NlxD0kGs0;tp6lAn|G9j zMF=CmeADiPnp<)v5?MHI$bI*RfY63VqJL7K!xvVx;bOt`+WuB3oN3ri1-jE8y7{5V zcUSUD&85#@(oq!*_e4Xy#NC$%zS+j`7rF8m*>;&fZOsK5d+aUD;R@S~@Yb^}<&C}N zmHy3u*z>H4U@4XGNjopAyk7SND2SFHuH~Y9!>JeuLa!wbHU4;sQxu3aM_&uWQBJzD zdZY$SYmMF){^Voos;EaQh`FRCFBBB~&}7CfDi4d|dYc)Bk-)dMpU8r7NMKFb=b!p@ z6RVI~>us%%QkEa#3ZV5dWq{$-Ac2|JLc(pu;3)w%|FDnl*)%R~iJTSLbKq?X0JmtJ zLzQs43f!=<(e>l-*!xmBlDs#vuU<BR4i>UuxX!)&2D`b3vRH)-Y};MxTBF*-{G2S1IGdkL-D}qLU5nK&y@Dw{5djW zgP!hka62BIiN}T9w)sakCNuRFShKBY<$nRKfRCBzDp@lxa&@~@l-{9mA zgg06DQ1-2dUvgH$=5O&t{&*V!iG9456j|_u@V&YJiigXs;QubGrsG8Io_*yu?)bHMHWz2iVeY2U8_hp+CIau#g7Z$Z|P* z>l!lp9Frrv;N{#m3n=IqAd^r0Kb41*KX`7Y;Td}A2LGQG*@r{TkIyZi>ssYSy{4#` z=|>q$JXJtW+BaxahCHjtN{ZDXiUjOxeb+V_H53qHgB9#Kjeo!@6W;qKnJ@SNg7asEwr?;9=>6`^U)-F=b=B4O9~f! z4%Plv=!;mU>*u5k|4|Yo?}TLEcQ&Tyuza$Ky$-)Huo}*Pq4IaOt?@&BnDlnlY~--$ zDzb~rV@N832WZJ$I4pVc39n0^ZIrrCprN0||HOzgfyLYcot*=uV??LxZLIqLL_B#GZ-@gh||!|gD>SwYc7!odGR(j`Uh3td@RfnW^XP5Y~0G`dEjk{Avkko zupD6xZGN%t*{27C%)-pw#Qe!DC(P8@gw*l$*@6B63!f!G<=2a&ZJ^>xLB@*h(}y~r zy&7hR-uKVl3os0h8wbmaFsQx8u?s?n$(cyyHe>yR$VAL@R?zzDuS|dH7M9a`JW*j` z(Kdm~|0tLR9=>`R!00E1Z(VTXHAF;`()TUa-MhKOP(}U$VXM5ZrI4^<>%}{t^@a31 zoqB|Cv-nyA>iL_6(NzEQ_xo-js$7w@j1A6j!#7XqO|KIGJWpSP-Uy~*xmI$sV;3V4 zHd5)At_&YY0t6AUN(bAa=j!?CkOQFxeOyqchw{e}OaBL44lkD}SU_S>9e7?&BPqnM z4Yc5EuBdoN3n(n9@GmQ_+S)|Z zot|ZEv<{q32a$96QTRapUk?)vIDIm9NUS>%px)}S?+p@lDyz91?2uMc+){QX zY0rA_0G+-rtCuE|r7h~Oi~GKOG!81QsMDZcYc2VBR%Sn((sb@T`{knR@6~d1fAHvA zCq+ad7pkzx0*jFzZ|`JYra?vSo!E#Ufg%_Jr_k{5x>o8fB)Oa?$Xk|6Vc?&)DlV{m zPV)s{N%>*wwYt%Z)ue6N7~ZR_-6ADl)O{yuIVU*f&~yqXEmxQ4GH17UTqjF}yaFg^ z&l-n4n70xeZZYH)gznCnlkdtfsc9$l-Pub2C$~RPmEfS0DfG zYdJpR7O9_EA5xg2*4g+P*R0=NEa7=~5wIB*9=@KHQHKpBBWYz9Wn{|zSli-a%2k8+ z1CMU>T(r^l9lx&H3zJ;;sX*yCmT<446iJEWQ6sj>>Hp*Cy5p(d|G%!jxlN@ZMW-7j zgo>~=i~RjzdtcE<93g}C?ce*toS?8<&G9Yh#}*hXNoCDq8Lp{!LPu9W=$@|r zU%tTo%4=JPlT4{Sb6_xs0uCMV))~C>& zGYsU`be-*i9I08iv-!zA(mkStGb&__JCg#M0SoHu63pDXg*kmTr;_aN;wUqo{q$3# zpj0-SaNB=`D_``E?t3JhEqiT;9#gXucl3(j-(M=JVC#PV}8Qk7D;!!!x`H> zJB1{72-uybsA(#MGYx0YpNmD@$G}lQ?gIyy`fC%i+X9mh!05{`plzRtNxqQqkER}% zkV_)-$uf50aTGrcGcz^N0ZDIohg+3sLZxZP{A6QUeRe_XYo21iT=>j}Zy)o6&w5!RW@W!9_bSfBJm$;8Dv~yV zzb93}@jI;8!b6sPu(YgwopxfaC&Qj;d;eJD%oWF_7yw&{um6UmMe6 z6*1so4u|vUYS3qPiF2xCV)z-rH~#?#jQnAJJe21U*^-$ab-V=|?=T#ua@WRAOI=;_ zemKUIRGM>+rUuXVzy4<#F zc&rd_eY!-ApP_6NsOV{Ke7r;iBAf+N25I>=5lyPR9_*Y?C7>EaqebZP7}MTJb2ivW=fySOXoaS?ZGAy9%1IMTu2X#oEu3;y>!5-%tonwk}>Oh9BmZmAsS4Y)E@%$_0snm0l4MmZGjMf&Iyk?ka9fSvWyrc zer_DbcHH^&V3>y^dhMat`B>gi#olPmRWItyo3b|$(YfH;^sJPA$3X4#l0{+`TiZ8_ zUnz5OZCl_k9+_mnEs$aw2KDw!n$*{2x{j}hH?ksDo0TpEFl8chA`;(I zEe2#dZIMw^+@Ee4@ca}f2woY7lBgzJi)tRmqtTJ=D!(o8!7bThrSa@*yYZBM&tSlr zJn|a6$aGJ_ZT_Vg-z>$oS|pb!vs4Pi-Kc)PuzYWmmc|>Q_q-Rz=Ne!~0t?E5!~Wrg z?x)}Q7kVctTQTw2@=T&xV$S&AH>`+6<0BkHFz zXu?uV4BV!CPkN2YFvO4&ETmWer+3sVU1jKGW(Gl(VQr~=!KTJkN~ZxWoik8q2;$u$ zv^BC;+&6cQowP0R+a>oMXjj33IZ6MnUi2}zZf|qaJaHy8B7Y*d^kbok$>sO^9;2!p zz1p11r)+v~y`EP8v-<9KTIAAaf?&dPL7Z*r73mauz8{cyr60Ba{dku?H71R0t`Fe% zybh*}V}{^?$9_9BPa$(;bgqOaEqc{QJ)s zc_LPoXs|>dCs`DX40YbHyd1Je6Kh&~9jf%qb?!r#;)7>Cg?Kx^<#ZdabTn53;eDNqmyq#JT z-N?1TT{;U^f-TnjWR;D%9F`o-%Dzpa1ng`scqgfArDldl zzIdxVWYb)$um7dn+x-dhwsW@aD{Jh?mDf26qa>DY{Iuske-C%(i2if)@SCuCNwM|v z7DZ7nZ2GY@+lst%7&op{>kG}%Yh^EjZbU`d6-5@E`4Aa?C)vQeeW1*v)HGAaELq^& zGl7Lx|1O&D;o>q2;xhWe&a?!1CEGtKMq{UB0=NMtk3<-5da+VIQ-7a+8t121q&8#c zaH`LUME6C!?LG8Q{fnbg4h2tdJ9`{=<21iP(+5!Usq^DQeFI#3v@3=5kHwpnv%$yA ztWKAl4%*3Uy5eW-ek+;G)us(PjY#Q*+EPcPe3Lb6j2{2Je`BZfxddHZIpO_B%$vzK zO@QSmqDb{EUFGe8-->NRZahy`4cHkZ(eF4}G>I>2MeXlw8wCjB;~v5f}aIZzPyK~hFUy4m~meR_iO@vZ9>CntNN z#*fr+E*X|q%$6SXyUw|J|1X2n?wICF{isrFTfKtLhaMQO7(I)ahv9|$=U(p5DV=gT z_*hL{+)wBFG2o#@a5cBmcZcs0eP|hhOM@Zk>lzrXB zvDIKwdtdkBkdM5yX0rawZ)du2#9fu(WYV;v?D5ux1nvvpc9j=a~2l~aCAzXicqfpnqChxQl)5o>do(zw`` z2bX31{(6Fnc&sOpFeK}eftU3+={%Wb7aQ9b{<^^K)hqvX*?h#GZmkgfd~W?Cl46}* zRd-dBkzxiT`hfO>&jV9iVf)aQXGgrNUEx zvqigT%@Xq!&nCM8#=p83FFjLR$_`lYPY64)Asn_4Sf|25^FPW&1zNeL^qZ!M3CZKv zsq2Lf%2sK><(Dw>?&P{>4w!`e^Hkz8>gIasoMN55J=7-`h)oI_U)guWCHZ$r&wyUT zfv{osdZT!SY3rOE$v!!~*H4c>7|>dc)TSv7l@j$r&BG2$N+r?Ly?mS-JcunSx_kGJ zcdx69b$RJFa&M&zv!dl$rOOj|ktaK3uPt5{_I{dKpNBv2fmt|iYEhMeRTIODX~Q`$ z+{Xcdw%G{1x4D759vYGqI`CT;oeU|2<`ZeC_IshZ)3;*T!6LN>hi{7T7Azw2Q@)r9 z&s_XlbIjljN0mqKt!OUyvd@lNRTo6Qqhvn6i%Ul(NRNw`u!BlJ-9c5Vo3|jMw=(nG zOMoE89U_sRB!nH;*bj@T-+6fKuSC z$Z_hz-z8$B`h$o*h*lmW?45hS3lI@~c(^9swm?@FA0x`mYG*=QFKn}jyNBndDjq7E z;u*I};L-kkjQ4?54>A&f93&VB)CYHh?SYZ&P!28@H;wKWG6`XU&zN6R4mSuEGF15~ zqELpO(*RO%KlJ%J4s~OX)6NnT8VN;$%YORTB59Zm!~dR^PO0GK!Rcd0XJ~HTtde(d0;OWRCXYGvuj^^1UIh2^97{cOrKx z4jgBmTT(uFBxJ|*NeM^;=krgw@|p6aL8+|>45RlJkr`2{2Y(MOI%zBiN2xr5&zWOJ zEO#m)*{le1c(!nF77L%C%ygV_tfEr;t+1$&l#QTvu@iG$Lk0-fW#7Tny8v1Ux>!;t-weEhxqc@SU>9isB6 z>7;+!xOCtB_`yRlu0U(a0MgMZ^U)B=MX0Ex@Tq7z zLuU`voCUp^u227V&$Wf0r`L#`Ab(xZMco|ct&!5XMuuDj!MFHq$0f%NS$)d$I3{_$ z!a*JXCrPBraO^Q}&%t|q`dXo&!BRy|%m*LD6I5_=IR`^BVjJGMn?Nb1ckc&t5 zZ42n9_okyKNJd~8>*G<4h-UYs&LDm-nQJEw=}zk$hVir5{r!g=E8^ z7dq!&1kZx1(;d|4xC?=$^dcfCLbZ_NoYx5Z;PP41zK}HP7wVq03pczgr6LyxrB~~s z&v&1i3@JG=+DFg-)K31BaF+mU#ohg9XEi7Sk;IR>U~7Rpx1TXr&?_90C42XXcab00 z11NG|C}2WMnD0tYv+w)K;Kq~rgsfn8km_t^-W*Jyk5L6i+kF7c*UY7VPn?;G1_~Ur zu82)4nBfWjdE|qQ-gJw6j)LRnUw<;%cBt*7GSlKMHS9~)*ZzX<-?_ZZA)Qx#W$obCXQJuEyk z(iJYFa>m{k$E_9cU-$MmSHK*}QqlBnndP|NYwA*0*9v$0@FP1|NAJ+9mjfLS+5l^M z!*HPw`|uHOQxk2F6{^hzkV+GN-n1UW54ttkLc4UT?b>`yeEBf=!5RKs%BWx4-KF`r;q(!2eQ4iUC&sr(}6|c+kau*1A=J9yS2J^x(vGw+3L?iDq7jf z)m3SR=#B^J&xhw6uTOk1y#N7c-d-|o;0&t(^AF-t?fvt&*xDuNv zKiuyV&u(ZE>x$GaVitYqd;R%qzIv#h?NajOVq&dvk~|%oUlE9A)p{mos&Ft#Ez(}= zgsA7|t{R(=EsU@~E#*R-E|yMCl)UR?9?F?{)}b~aEKzIgS-dT9aJG=dMn`(aG#yGjwq8xhPuHiB-1MEW@w{=e+SR!G`*snr z2*vNm?{6p$MmQ8v!-~eY1#0J7S~9-&3IEDs@4TwBNA#Q26dCx{c^&9)|j0e;b=${!MnfGZ{_NiTC_7BS$hTzWLVB*{!m2Moovpf2YcCu~x zOJ68Cq9n;7N&q%ybcN$EBxAM5Vn)PHz$GE~dA&`#MN2Zlr!D`#36nlY4VUwaJFebz zR_oI1pNiXpPyfpl>x=9EoDTSC^)c1z1UpA#45%*fxuTw4?6sJCf-^;VD1UwWD?xQtXHl}~M^ zbRo}jy|O^=)|R^StciWi^NVMa8@D1_>}D-3|Aj-ZIB6-!#fCmQHFPyR#Izu^{YSwyLFr^6 zB;U*btAXO{1?vSp8%gmixOAQYOgIuNZWcjR;=T!oEb_B23-ny(lv4R7F& zg6E57&8Mxm1zMx|Bt5EGc@3BYsv_MZ$KRZbE`58i(^{T~YsB!wQXTot*?nv_2MXtY zeA8+cxrxQC^1eZ`v2o>YX()i`vVU58-b1pfrcUrqU+xleAorgb<86Vf5Lr3q=KUOS zNkus!n!}i>pdMGj;NiE{-4vUl#>C~Raap-1Z?yL>V|Y7Yh?Cg~uSzz`vWAQ?%Y5=XO0kxfpU`^A?YB+O zQ^h7vX;Ei=V76~1dgV@M=$!sSGJTRK_bhbavR;&M*tlNF4ip2&_W~=ooYNV!L*(Er z1aavzi`^*xp=EY zGy^V`07;BZs()e$eKbFT0^oel!~A!|^hA5Xp*K`t{szRt@uB$`kKqX`o(tsQsfmS%%1`yxB2d<@MphR(*E#1%roSF)a~I;$4#=3kF|~3*sK2LJAUT zl29vAw!9yt5MR0N4sA%jyClb{q3HEn6?L!soy$%tc%h;#hixRorL;Of4gs=Tv3 z;t)fq*}w)xdKyh|+bP^Z&3q}%fVaFkFXxE4!q^HeX=omfTq;|NaXfOU??O{s;ov=_ zxaU2$zQj5}eO#xu#{46Q^;=j~=h z2BJH@mfQP2=4*0quRif?4HlI(YdsR&(4TdQxYy+T;YO9`wEGhqaTgkRe zN#DH7oMhiMKBU=}g_w7}P)an>_^Oq`u+(-Yddd!AyT`q6q^ zIauyegs7hc#nZ$dY71;AL8VflcL4qwxH3ac-}q?9oi@j z{u!94G?O-9oeGWAUB6E7yQaAY>lmDB2^LT8Q^?pqp^s4dE8yk}Fr#c$D6)UoAOuzPgWjGyeM8@trh2-la)&lQ3S5OnKe<;yDFgjp%@LL zlc2h+rT;R2q%7t$^NVZdNUJ>%LJ-nii|OauhACO|4HbIwb+OS*fUtih|8tiqk*M~Y zp&@8kAjM?>qhXmECUz&t;#d!RaI|)E+X?{13s1`A!Fv)s2#DdVm|ZvO2a)jf(pq3W;ENa=&gXmQ-(T-0zVV+#6zE*3(L33apLC{*Ypr&QB1YlGe3&gF zb2Sy~e5M9vB$-F(>e~X1J!5n6y-zm<@qY|@U(_Y!u|nRh$R$2gZUl(MA6M6j4hg@L zJ5Rg;DVP5|Oz(x>=O-}xlF006xxkblgRPWoR#7ZFh5rz*$Paj7w{iS~ZtRlN@Hd_C z!Q1f__mC_kp-uMzgmGs_>dlUppIyS^Tr6=a(2F1Q#}|c7FjZkUL+5;%K9k`QorbF#aA8+Az&k5?3{gV|3p4%yR%C0(!Lw^;q3h-;~|9NkI0LB-k#5ZHZ zho+nhkp!;@V_)g1v{7Fg0C4b((@se%$p$K#X6NSihy~@M1YbBO7&}&%zFH}sblAnO zBySFLwgqmQaE*(4{_ES2wgnCup^Nyr#_4-!uR=AU5^8Fa6;=%?L0n`YBeN5~!dRu@n4Pz)e?mY@!x%2+n&RoJY2{&JVS$~Ma_cjE!c=x_4Td(}QPqQW>gKzDOYEmFt27)_SeSH1%fKZr4 zT^gBh@&PTPxLOM)0VF>h4c2yNV=Cv?^W3?Ju!aQLQm&rnr?;-nAgX^GxEfCl7w%)w zCORXBjH+$rC%{mdfOAKtajn||zC3WvwtPR-wAH1p)GMnk z!i{S>-_V>ID#H%PTmO|WeD5OgnM-EYn_a`OZNCbEZKcMV&%8_KZ@DCON|UkiY$)t3 z^1J0~myYk@w8DEcoVW3f5f2JMTr>iI`PUxk_Ik--$6=Z(yx}@gH2cu;9rv&Q`5*Q? zW(5x9;9cZW)=~V7SAH%63t*i=8tqIvzi>+KgVT8<-S6%%D%%e?H;)7ug@?2j=e4Bi zPHzGf)3NLOh_9ohSLK4<70MC16&!FT6W@qmEg5;^8&biKmCNYv&S$7 z)5c)~>OQpOTLz*JE4%jmK9wy|#G9+M+hkB?;QeJ-ulC1EaRPecXwwVw>fT94qOW5I z7Xph6()qw70$**8xDC*dzkN6N01eJ1l(-$|5nYtX$qKrZXy$J|xk>9a`gi;Y`b-IF z@?CQH<4aFD+%e5h9_#IGQt5tuN|Qt7bO6|0xsa;>#xONYoMRM%FCq9tRJ$b(cqQ?B=xNEU!%0?p7kr4!|2b z{P}ve(f`DL*KY_lYp~WG$T;mR^;aZ*PEPvYlEEB=V}AQtr(z=z>4`-1I!~5rZ2zw; z?8@SG>YH$M+}7`~J?6f?;t3cJhqZ=L%bQ<>M)p+piLTn);rRX$i0-Ebzn6mKojr_J z{+ftl!ZtY6A2?Jz#@`0=_wCOipYMVWj1a5fu?0x;M6%jF@NBp>!von*9* zyy@^*+ZF)vYzLhCRvE>B$oKf_Tda~yJs2FWTS|T48@JzDt@qrb!JCy3R+RE95D5R< ztJ5O}6XrN})8y*>@vcyc+EU!`v0LyQnH|Dgv&lRthhs0;xF&+Inf)_~1<_Rb8%8so z>_aT*vqxk34J-;}0R1Z3Yr)ML+Zu`l_zQ53?XNaIQvTOs7(8KS_@EwQc^)GI#jtIG zgu%!1oRN=SAAxjeD<OAP-g2xcvI;5Momz2x@!2i8j`-ujUgl@c#%) zR>$UfYbJJpH{}%@{f%I!?mW(|z*H>=A1QN3OZhcRVPs!UfkgtpXj=NbPLI8HMI7K} zcnU_}A zy1F$I@2reoHpa0B;rcqS0pz~8SzVFMJb!KS?YabG*1{+a7o_jzD!I0+GFr1gi>9Dj z#V&9|zC<0}*&u4lqk1frT4>O^H9j|Np1# zST&j_TB89Sn>F89Y(0_AHJnD}%)$S(%;ImK(nII|j7?N@C$yi%_tgouj3MU>wA>A( zpyouZzhYEI(aQXkU&nBAZFKFq3dQu=Wfr3daruv9981=U(P`!5H-y~W9Hs-lk*AZT zW52Px{8+}xqTX$R`H9?g)V6>C7pJ%_;5ZdCaXhW=?IVA_$tC!tM;+<(4_xB+*v%+c zS3HiWUhtBAt986)#VHCJyJ)&T01brSy&i@+gIsf;O$v|u>j0{;_tFRyB|2XGb+Z#sBn!5%7?3n-GctlHE=$3!~l{z8LVcH{~_9-rF#m1;; z9Qt1?w0g8lrq3yfH_9N|Z3`Uu<>nn_Q_%l1T{pf7ZE82wrjjV1)A5a3V+~s+KD*8n z_Ig@3?hHB$BJ-S|Vh=`mgmvTVj{L0GR;Lrq0QUrpV)(#Y*1ecj1nNc}FLOfqG1Jzf z_R?0@IZUU??6A)T2(1mVt2>XU^Q$&Ry>6|YDmq1|^H^LUhc7dV`KnCL-CBwqN?yc4 zWKlZ)2Cw)VvII!$1&VZR3rJPUI8Ox^_FhJ2um>HvBqzblSg%NeECSUiwD>=p#7B-1{ z94_~HRn%*O!z26fgHSq5>jaJ~O{VMwR1d-_aCes9%x&)JMjvSz&M9p%Q6iw2q#V{> z=uhJ~%%E&khTbp3hveMBo2Hvf=>Qu9Uv3#Jh&4usg5OgUBAJ{uyR`+8MNlZA5UHoM zm$Lmd1VW{GUjJrV*g~{qJJlfHw5y@dFKP7%geML)PD$nD&Xzmu*1qRscnMTfeBf3p z)BCvF0@6fI6wRxHI>f<1PEbp77@JpO+yVm4GLvIA>p?X`l^Ew0Q>-}bj~~(X88xVq z&r~a&`fowp>F8YfPR*`v2V&4OA$#bOi4&;5dC1POjg6AVu~zf*>(#&n5~Hs8&m?Hk zvVerern&2_6xhahrQRZYKJNa;`?5pvAX6=i<6Y$HHXI@Bh zG5iNlSB9d4dl3C>z=K*uNnr*s%6TJN*^W1A#-+kFiz^1V1*QZ7Sx2Qja_{v3eTZrP zkG;dW>hznsusd7j6So~;5`;FxwNP~4vQKiSau!+kpXE0SV=sDzMrm|wO; zkUYwn+nB@6HQC%}mvZZNt;u6njb))w2gh}xD=o*vLtt(cCcUX77{{K3w|XkO=Z-q( zjZo6;3?pqXItD~?{WW9rq(PIzj4D8H#ckC0c=yX#2EdSE<9Vbg+;Xh zT3}aM^pSCb(0|H4*neAL?EvT3Ls?( zj{oya_6;_ufc<)AdmoDm3eM5Tn5EX)SBc7@8<$Imf{8g=?YJVaNM|wFl8)b6KD7DR zpEFVx=qtTdabuJ|H_5!w#A6bUeis(>+!pvEKL?V02T1r&k>V2{?E&tUz41UH{fm@; z%2$>eAKGszCp?%{e(s!XFEaY4gM*HE5dCmR_}qEo%=~Ez*cUSsK{hq}rag%0alMS> zA_+U0_TRKcfoc$2G?|A}9LU3%U%i+$u=J6=F3DO{l@(lQ){|W!Tb4#duS44c-%(H>yKsIf`8wsO zRzrQcp1T5pMC05|J;HgidVOc{f|jtkJ=5A#Ub3XkfFiTBw}$x(EY?xQErt*bO&|UT z#-+fUS6-;b%_g!na$0jOS9)vYgx{rnK8ub!^qKNBrdl*cIaJa(jiJM!`JQ|993Zi0iH-IYUL-xlyz>r4GgX=TI8Wd6h(4G0|! zMwnkq8Q%;ZpMIfP9Z&K~8csG`S0_B`Xp>3{uIBgYKwEPhUg*)-FQ`1NJvMQ&a*cJT zL)vMePtcyYQFQzxNQ9h5#M0-PKIg`Hb{5)NpAHyoS(LBYbzwY$uu~rzdiMSn{4sm| zI8dF%FV+XyWeX3%p*lO?<{pw#7CRfk z)ix%tL|Igb-@8Z10;$?P+%V%v)05I#O$eGVP(?{t+XeiWts+%H%J*&*^Q8@oX zdOv9Bv|>o$6}*hfz#E!hAPn0bRbCxzf&iK7U1#|28qi|N1uNv)| zP!fUJX=8+Ie2tT4V|}#qiYs<|ugrj3pZf^+#8759IN}mtV|ea>rTIgG(hP|l?AyPf z6{#!x$<9WKzii3Cv0YwR&jjyGs1L1!Xofv}A`#LY@yV-$GZ^RX_u|az>{v!Ki6+X4TKO#Nzaa=H@OO=NrI*`vW7=LE4&Lf^_1+iALBsyG(~3;O zshoCPssfof=HkC+Z4s^69=qVCSMu;s_d;|4?6?+cbB}*HR6MHwfhH>07p|9zo!!fp z(7hUjw{49BhXTW#$QQwJ9p|@3tG_#1OCpvV6<_MOtYO%oM-Bq&Rwp2jd^Pvwr&nu- zkD}WeD!&|8|NT6B9T8nVIk`8SAlDzG8rcyvv-5YL*s?5QIZ|mppvD4fskeT?ds-IK zD`J$Xaw*r*&Qmxd>|dvn);a4(@||r<($>zV9)nO|9%@BRhTykXbDfuIS1^# z5q~cmYybhpA$ta9EkE6mF7Pkx1>FnE&`Q!6_xm+1je{Y)NZH*Afw9omLwKny>Qx2u z!ON1>0aD>`MNDD);iA?bX2uC%#hwb?^4Dj9<4>j%GKAJy#koIW7M+vHE|2>LwHinu+q zHYs+r!+fvCqaJ&bO}?{l(7YPo+>TMn^A?k~i5^0avpy-BDT#A(~Cv^8`Nhwf;ad=y{7W;J1WoB!P{2r0Qdz zkS$0ZUcVCi!A$D_p~AGGvdgxM0pT_iQOr$?Gs?h=!sjOKYCUUhCcn{-5!8*OD}SJe z>-EH;DHJR6p`3QtV3{GbuhK0v$|ZboT%9|(GB0txd!tAcQgr5Vq3C87AVH7T#Xylr zdclzqtUf#~E6u)pjsHKLesdYg)@0~dKdvJ|O;h;h@Or+1ncDEC1nxz+bJZ0;eWylv z7mAho8u@hD>z|@+fd;ZFiKDlUn6FoGx!leM1@rn*Uf33RsU^;cRvs=kxBZDfDSCO9 z`x@l<%HEg5@4KM!8$~sm;fP)iTY_kfJC*nTRQr^9^A5>$c0!fxM@5p`KsY+ZY5Wqd=FpUAfJB-~TS+ky+d0Gkh?I4xjOWS6 zi6{K)R0cUyG3)zk$GcVrWhk0SNnc8KfExZO^xGoV4|Y$jHuu70#U04#=6R(WRAim~ zDzce;9`$=gmPyk&o3|~n*RHpts#r73GH~pW4hiiY4^m!#_qkk@Hv;NSm=IEd) zQg-N}Eg(6@^qBjS>_Sr`8M~~IaA}jWWjbPw?fOs_3erz)j?^)0UL_@l0-Y~PG?JQN zb)23$o!K>n>u@%>|4`>q#v}6G8t}ve(Se>kW2TB$h}daN6~%0FQ5X#(aN*bJF7nlr zPqZ%IsJNfF!!tR8ibjh-2ga`x{$OL?Y#xw9HkFbhe@v#`olx7kPq-sy!MYjo9o>3N zI+LYQ7yBJJaP&ik(TU!g-_MZa^St#B9~%>l6Fmm9f*)DhN2>Fd-ZZWjewf9jmUe)I ziZolEd)Prs>(_UYPR(Vn1;altn3xf=HAbLSOVP;EkIW~!C9l*1mSgV*n6H)XW< zYvNvsRr<+BfuyHDekbGymIfb^3Ky|))=N_T*u$7L`23*j(i=#2dVO2q=+dc^jp=-m zHIhf>Ip)FdMVd3=E(iFD753U1BQqnX{;bULDUT*b;{=WidYqmY*s*0L{;_HBB#mGKSe10VT)uV7@|nxo2iU zb|rhiXfhY8zm$`+DU~LfGWqG~9i+fdrq zye!$o)DAP#$Dl3;_!o+VfxKAjh_M3H+2+vjLCHqWGtl1wN|RcUhD!r|w*6Zc%n{<8 z=>I+M8)T6C41Ybs)Z-;eZia+era7)|6n!#4#$#qDCYv-tuUSO6&Y(t`>40v&V&3TT z`?1y=OaDnd zifkmW9s?T1Z)52^axm>bPv^g<6VmoF4TSOEE1DwX_2q{*k?SW;5cp)~rs?+=Zqk#Q zI2M^Z<;*DB3vyL=9&%6YvMhoi`qsqp!J&XtpZN8}es`<0fEZF!pPLbFZvK$^mtWE3GC(dvXUJ>{?OX@F| zq%k0;+Iuc7mLCAK}Kk-2iONq@|*T78mA z$hjJo>P7^G0KHGI@DFX&QGr36Hl;wuCZdy|0vag$I<8J}o(9cU-W;$^7u={G_ z9}4aP74ze+|3Ag;dDc)fMim!D3^P-TT01;TW;Xy`O#JLfRN(ziKFFmC>FJ*k1MjV) z2-4k{a73VhI^_cDhj8C$va?5+Fqh7sMU-fofkNPk`D<<>)7v|8R?|g%RYx5PKE;ot z5B&U{o%=k6w=j3;M1d^!KObE(sGn?5OY?cqGPkgME3nfL0K{}Gi4)2y_-=K+GtIFH z2ll^6uJgR^M0AtG%T~Chy}~2RWn|7Ry>yVPQd*u$pCZ5JQG0t}m|Peh<>1Mjyg%ch zzcLMWLxJ}jFVW8QUXFYM>W&jJ4kt*{NQ_^L``oL&Uo`gxAMdoeivs^3@$wtwz1ANP zj)zzYoG&sT<{8^Rw1{m^xfBv&U~3?zwCUJ~)r;A{R&z9Gk84i_{yz2d_Qb*twqhgdJ)UDDAm;3<*F~Pw!LKmmGfJeN`eaKoi;^(DH>i_VQm#`(PpRRT1T8E0GvXQyx~>Ll z`rCVjnD-@ydHILpI-1XZ*8X??N7P&VDys-F)l!?9g$xhF6zzrm?F@(O)UHQ@`)X# z`~}+I$ONw$rI0jalN{92=9u^`j%j{ES=>rFwJ%T8;fm+Id84x_07eJ>7T>jEoiYF} zLgWj=8|A1?!@w3bHfgepivM2L<^LmTI6EqUi$vH>_Owrv5X9NEqZ9l7E!kn6l+zaG zcTq-DLbd!Z>{g4nEUcfJ$W$aQhu~gqzMTmcssxT<&x;BXefBb}I6a?B**Kad8t;@c z4}`KCYD?cs#;xv(x)VZ>czMb8`L{5jGPpE;h{YSM$p(?rRC5p3!c*^3Hum+P&yl1S z9*#{IuHGm{)}(A6L$abAZ-Y(`9R3reaB_(oR_H!>F;2MK+$#88l6Mj@%ZcM+A<1OE#+nU$k5LF*?rgC1djCPL||o6I`XbI{rel zZ@=lp+yet=?m`dX(}%J@ufWDqrE6UnNhmZ+NLoxyU|-7cmdj|fFbNWAC({o92$;_J zCsg}Z>PLv~BZk1-v~D)H`?fA=`?et|t$;_!nVf?q^8KIoh|T3zOB3X9Te#wZ$d&^b zOo7K)9F148%-3cxF%Wb8{rF@Ldzy>0cV7iVh#OfdiE`r^x=emFB6!Nx{#m%NGkth8 ze<~RY=U1)b!`eqZ*@tOLkapc(s{~XMOHyH!k3AoaoOXexs6h;2zH^hA}&8t38s@wVVxrw`cx1&xin@H^)hT zq=(bS%#0WU7)C!EUU}y{#T8~e42&+%8A%Aezi?&sU2TZ3T>?sl#3E7p9Lie!RE7Tk zrt=i?rmt$fW?4CcRHskf`kB@aGrfYqLpZK1;Et_;giO12gyfHBBS{nA^~C0-akCDl z_z_2u+taxY#yt7F^t#b2uV?B~*SY;H`dpr}-MFy%v%Y?Z=FJzu68Nle&okP@GS+K- zgvsBO)s2cjq5O3V;8o4rfAZNbu|eeY-0%Rzd>gW|koMajD64hxdTrm4S3gxKNOIP> zVvaOkc`u|H@TKHc>^eu1qs#%gPusrUDGFKK%nL>gUx^1mw*+o>`sfZO4NZ_}Pwh8e zB~n0Gh%|=AZ4SM4&i$PW?O7>`?&!DS7G3dHIH#c)DzSX(?BEO|2dsDYh>2R;b-O4J z{bo;Gl?0l4_hWNSW0GdY4P_Hm&UZa^srzeB@H^I zNAB%B25EP;O!Dq|sdrQ3@oJq9|MwE|W>WMH>)9y{ixB=(V3jW~0j4`Q4-7b7#7+}z zbVUAEyRQ@azkv@W<2~){+vEH>4?|}5hIrV}7m83KE%GB7Zv5WC%N$;;^YU@?*_fy% zEo``nO)ih)}Jn|}I+9F+! zZOn8>^E)ASD6E^g7tc<`k;>m`Iu`XH=m8rXT#Q&K|Gy@e29yD46e7@PYe?f&8ovM3 zTYvRQyUPRupD=5FmhgWZU3Wl|>l?MKtlRLKZ3m^6Im+8|ltRm$QsyW}R%-6OaeR$i zmW$NXOofJqBQ^Ixr7}fDrQ8c~Ax=xZ#aM$`Wvks61gVI?eM-;i>U=BP^*XZ<2KQ`Rm0Z>W4<}9}wt!xp$;U z=nfdeH29ZB%@k-*vklhm0++KD`RhM%oMAl*P*w1uIc79|g!T#O(sX=l?`(KMBaHi4pDm z`bA#Y>WW-;{21t^!i1ku;Bxku@DZTCokn#inGgl|;L6pt&n?nk+AYWhqeLF`XNF|6 z7vB0=3v>pBNPR{=B=E=28Or@>;urVll(*ifQwjBh3bF9!Z`iir&snd8t9d)r(!d*( za@{PTDm=7-Nm?-IWGvu8*^6&_*Ni8_CKfhPu=p6vhL)8dD5fd$_p2WXR(!+(uNu#N zCC=Z;fr8ROlCK0@KLBqN3MGX_eCE4rf9nc`xs^*Z#&D#?AGjdL63vC}3rs;MkV4hf zJXYCGr|?jtr-o+rLQ$hi;6ToMuLYmpA@gc8;ZZh?eYtf2;8P${F1xk)HUd7bd+T6s zCNra0VF^9Nr>^flPgp!IT5PM&|4E0}*bnfFg#F80w(j*uyhwZKKM|`r1J4murVM_) zo=wWWsF+H*J|w4zI_Dt$WJp^&ddDFsRDb||V-b3QAr!U<=_&7)P$}uOU>2X~+gE6? z&mg=GAIyN+tM472pw;;N$hG;vH7zEVp{EUo!cfYB>XwBTP;I(SOHxd5kJg&lXME#* zo5U3nrrjSkKqw`84PzWWi%d@5+~Uh#hC{)d9bk#s6WoGOggLV@?Xv!KgufTJLu5keNZ;?V-PR010}gvQz0Xyr<8?PnX zbKb8O^5y-(ujfVwBw|e%@G})xtFUHQ`>;)KQs9dT9 ze6s~=|8`CBkso{yE_o&#xV6xY3F{q{gCJ+!VVagEb=%Wij_}W4S;98dqq4oFEnF7l zcRV<09Foypf4EydPLLmC6Gv*0n}4Vyrm&~nsL!ldpqO_J^Dg|=DPm~7qvzBIQ7tjC z&pLCZPC_O_VCzr4bqz5B9i(met`@$Xn6>V*(%)@cn5^qUl*Yb_JdTQ1lMDOz-N;FE z&+^3ME{SONedZKwJ5hkn4!;~da-BC80JGYohF)ueq-15Z20Mlw*G-mU2*<$!U9ILE zuk)T~Ct#%NhgA0iw6CJgBFkdhoBDoPbTyg9hDKy2kfjR)NKwZULN-*eqDxU}w{JCjUq62()AU6FP}&r7*BUL9XbEFVI!rafWJP7d55dMnFD<=z8(7&?!Z|a$<<8*|1?bP_}c*BQPuL>Cb z2U{GKii;cW<1~-1uv88{Cn&i-!d^Wz&~%Y@?l(|9MDY6_3R#~JcHhkZX>mQ?^fNH* z71BOcHR7>ok>Y9*s7mFBX@1x66^EUh&|7S z%7$KUDdwkn=3)F}Gcq=M4`rw->9*@lGgCZY5-M_ts10rGKH|sZNXz&ePqzalt;HSG zTb!9HF`cdA2+#t~9Srp)OJQl-dv;I>FENF4it#8apqV4f#8lBk#w zx57TpxhJe^kI`f7o69TjvvAB$*VL2I(I@a5RkY(V8<7a>7_Pmqq_oTeazVCT#q z0c>HWz`YXU);I>`?uAFt8p30@DQ2vK%9HQ+8g8n&FS%|Gbh4YEwfYBw=Z_WxQ`0_@ zZ(?lQYRWnC{Yzeh4K!cYxpsq7_fX4F?)aC275hNOpTZ?NMav9bH4Dg?oD;R2vD$D{@me;AhME?H>3)5_=HSWtFDd<@w3dZ3VK8 z$K(amOZvr;-l_7a$A^Uxu1_`iGu5bpkw*Q65A9FG&sEh=zDDDv>r0jp_}+oeAtN1QV^ycfhUan;x&NMpWAUcFBRjNlFOmZtN|3L_8ID?K2qok+w&n5 z-!}5z>QO5mQg@(Zp*MPt@#E03OjQq)REf(is1l&RqS^clC3QH`cug;_j}7g7Tm6 zRs>>&?)m(yd+)96f#wGj)*bZ$jJ_G-0Y9vGXzjcOzJ#{X@SjLz*B`@Vd>?o?CrPDq z!3sF)I&Feizr3)MC-RT7RG3;>_{$}jG#$s*>&A*DSHkw< zjTxzJ-ilk6nzb_?)+s*woIEV9G?A|xR&su_6v)&^H9dzQNsE6IK$~$nFx z=QNeN7I&4Ui5nZVu(12Auh2tqvb^v)hHi_|VNkSOueG3bJ{Ss2H0@W|i zgWHyXCvX08A`j15szZsAneFB!HJRdOTG^1z^5{|#tT5UAqM%uo_GU^wG!YdDJkq>FE2;JjDj;wHB4Wa+} zaQf53=K@j^pA=fNO1{xqu|C0%WmiGC%!}sJ#}m&7yvjtf7>B`d2}iQ6A6vR!QAtUP zKZhIsPegel`AEWu>U{i(WRt9G`>+Dn;yLfv^OOYgx3R zMh!GUBVBB5n>I6(v*f1_2@6O}I`7DBH8zq-gQd7LzrY7jv z<@W7u_v{7XYCx%^y_Uh$Ik)opd>bdJ3GhX{)$FH;v4|q#5(tTNxBvaWZnio* z^frb6rchMoHp>H6t0VFtytN&kI6#88l0|W1Otd8kLYXM5HD*2HMTmfFK8n#m! zt7o<>`*TASu~&6wZz>&8#y?noogbPi6=cYbeDvsE$h_F?)!fgH@NOz7*~vxH z`1c=;&8x70y{lhi!{tW3OQDcl1byt@))TD;h3$OO>J2Q{!bXO_`lk&rXyF3k{m!Af z(uwOxWn1&Y-Sl z+q36^yH&(+B%WDtTDuMp#1t#j=p5?uR7Ux!=T96$6?hsbe*U_6cQjTFbSX{=*sdfq znS^;hT@4WB`}d{()|b1!#x-5wI^KDLt)9Ik)XzQ{^CsCTl@&gO`b!}R5I)w?fHog` zEsHw+CI>;Gmp)<)lPCm-1Of4;ypRq^80=ymGZCTSRBbw8+{M-qenOWg53x1ge0V9Q zIs8vgJZ|gn2|7RjUp;t#mA15l&P6z|^CZBoBQ}M%4r)dx4C+b#xRtx4D?J_}c>@dI zE7D-`?q;GX{a~}Ce&8tB$Xe1pWkNbz@Z-f6;gv&$DH$zxJEiWST)s2bg9=)GQgIS} zp4^%U+^XgfqvplTbgLGXMfdm=4jZkIe1vszm+@?kaXFW~EDwx&feHbif zx&?{N;4dm09+Q@B}aX8jrGyPOD}=?e#>bzr3uqAS{`Hrgcp zxD{yIK$~{&>pU!9n=oHx;Dvmc&v{ZRbII?l{+zYMdVDMAQ9DwHAAIiYX2~LvCmlYh zi~gSs1?Ke=h5MO^7rCFYFjdG&!gsCapM*fF3S-!{_q}IDY<)O1cSIYitI~_{9(l1} z%fHLTPp=_SYml~Hn5w^&l1vzA^mW=xY#y;_jBD&;leJV!BpD^I(Uj-fyZX{!HvEo_ zXeOqrQY1|V!Wz|)&WjPfybv9z>623otL7}I8J#y1!dwa!Qf&02S|6!K|2};+(PxKP z=ld{Y@F5vp7zlItr8H);7aCYhZMbcJu3jtodeKG5C(Z@mgkNW&QmsASVGMy7wJW?2 zyM6-A4t>KL%c}oJU1c+Pf@w4qN_R9O5tRFGd|c|c|oyo zR9|yp-4c(he?*%D;c$W%ojOldq?UqaQb)5e@-8``BN^19iD%xRmO#X(tqFW#jJcUY ziAPAb3qF&WaK}MrI|#fZRI@up{* z_0T(pGEi>6p6;!s#d7z*(tGMW53vx0_tb0fnrPSCB2kXLl3;#(@m(lXlT7?isV z^-8kke!SEvQwk1i(s0Q2%UVHJL>+QRy|N4cK z<Qeo(~@DiO4**Ky1IEkkAZWfOZ8X3`RtfP5T=w!gH{sF zAg6xh=a<`fZA0I}VSc;h3}&#D18Aod4+K}LC)kilt(C#vX+inN6c-gm@iRB|{SUqc zzOiy%JF6e<5+k*#BvkYrFOBPJPvEMv=|<>V@n=J%&B=FD3^ht3hSk|c4vm@*s#8gi zw8pjUs{?}k8u%odF^OWfAmzWjLR-e+7a{}i*8gVkNX~c7(AA7qg67JW@8}zaQm?lPk+Z zCA0U08R0Tp;NL&V1{J?eFI78&p@D--=gww`ci{m%bI0%_ECwWk5f`o0H|l7$uBTh% zUZpb+99G*vp8i-7Y2tI9ze8b^4k5?Io%8q!i-nMQn-C=rp z5I^FPu)KQsaM8ukh$J_Re2DWKdgI=0&nR&cAVceH1j*N3Y6f~kB+RZW!(}$1@2Obw zZ}4RrhKAbLqWTejgu~y4w_O}%K%&u27o$MhH4_F^ObCd5@;|!ECvxff=Nftkl9X$G ziS=|MZ=&<96@|A3_*wTNR1|6XL-|$?QrB^Y7<{YiNj%N<_scBodeG@eyM8tf&#Zem zZ4tlZ9N@bzyX^H!_@_Byq zkOk);4t1oZX(&C{-$V5rz#Gwb8A;B~Ey9tEm6eU|Th=g#83IbXGV$K+q0D9ZF61Yl9y`>?a2}q)X|sn}&*@WfvD&6o~`I-UbBiM-H~# z;dJpq5o>=*_^#-9gWy#ui(xgS{h!D=BRi1((9FB#4~EMUL-F5Xs3~g`Y9O!p63?5o zp2}7izbYht0x3^$41X(Ar3f7hB)x+(Ma#*rYYE6Id-7Bz7^uPM5XOJ#--^ z|1dzzpmO8GXp#V83g8R!)$VFs4FSG!xyxzacYolax>dmG>idw*LGbaNuYZC-2R>qb zF69X1TLFdfZug?sP)lrxv(Z&S;JIymsIg<&8yXv`9IAA4_ef%)K>fuV&u%q@X&S9) zpr(D>EJy`XTY=a!T!cZc%sm4deG2TQ*28S-n?@NW97BUL`-mZ6GpK+{*ChBH8{i`K zU^WdfE@ja;`#Em)A!M@Lr~gD`w2vR${MBA95_rafu&io!9ts2vH?a;Q+dtQ6eLsQ4 zLvm+;QScUUG{T_rjj&rj$F9Su>Yhr64Clg2M=(WPBp0P|Xi~a+>R=KpoGrMwy5sq$ z#F)pEQ44!L^zCb)+H=?&d-j~;nMc}@mWaW8qD{gAZkww;lqx{ObEEvxjn({9XKnS; z7m(C_?#pDan7^>szFm5YGnP!knW((}t&2@1c`;2RHfe7-PH1<%%Ka?PsB3C6NPg;N zF_Z9E9Qo$e5&cqDJ!2O!mDHszt2As7YXH(^opSbCrc;hVQLyXohN9L2xot-!o^H~q z2H;2yh0!A9de^p93&W0>kp9poICp~%WApjFgdHwDZ7TT9)2Rvd6ts#khxSA|kQv=v zw;qssP}VbhaNtCbZqx($$J_jILemJe+dQ}%T|rc~24SrCEnSFZ*!|PkiyhQbzVEZD z@rt<|a{K3z@I)SZ_n_xcxzk~vkm&}{To;%#DtjC_!86?LuX9|3PyC}biNO+bR*Q_b z!_F;cp`=<^nHBGR%d0SSx%K^OOfe$}J{qROM+GbhlpE1oMtcA`ZAX?1Uq&r1(5f&2 zI=eB);zuT&2TvU%#`w^8{}2>zYT5FHeC5vEC{1z!=VpC@)XveD+IIC^;$L6o4}p;G zZwK~sI4xZtO|f|y!&PvgOlWkg(vIswZ!cDiZlC=}4+Fg{OdVrJF8~i_1n!3#TUBg1Dx*uM1-~z$4y}{Xr30=Xbu#N39*(g- zJ$ITE*{AgV8~J$1tbfaeBtrXvTv>kpP>9#zR~%LItJ1QKGV5gRqFm~QmKmJb(&=2E zl(n^VSwqX??lCXNq~0#21c~edaJ?^8+j1T+rX5BY$Yu-VtojZtNSZLM-=`V9*na12 z%jg*qp&ll>Gw%5IoFAA%g2ID&``Y*y6IZ*nNh=ZPg&l%h!{#>ci(MO8W<&Bx`<=5C=}3 z84%RLZj{rb&28yEdRnas)UgdKlQH_Mvjel+xnZ~wU8g{LhUhcmA!Y)+zvbS!a^buC zb5(PuH-7O!(7{Xqc;MiFBGDfUK}Y704gEq6fg2AfAD5y_nIJYi5n@O|zR@H)n#k); zByjC9%5m*oTZEBiKfO6Tai&=~9mUr|X8b2&sv8=N95|MLkROIBvOZKB=r!I)=}#97 z+zlqR{`0snRBJBQH~Nbe7cYFZpxgB86@ra;=sinJ@2UvhfXlefAU9Ao*_Z9>NuVH% zf;y5vgiN4WkXY+wl|;>ngapA%oXTd(?Jsyx)&x%tjeG>^m~qSG1MeN1bQ3l7;o~aY zDs86N65%}V5jWMHcxL_*EPdpeH*d&9af2neXrW~2p?Ii{T$oDWVe5g8W&S4#JJGkY zaKm$Rs}6PzgoC9efOw0mW*APFgCY6s>i!6L7r>=&;ttGwT^gUUREKk#T zTyR=}zJK^Pki3Y2f_sdNY70%3q#XvK|b-jH1XtsFj-_4{3}nVP->F`7f+alhoy8|0v~ zyS3Eltc&_W`s#cCb*vG?{^uVpyh6oV2>`OZCgH=|zFZ4aOPd;&M^1VYZ6y<$M>(+x zLY1^pSL2Y(-(;)!&gw2hzZ5XtrQU&@oeW#PbbDd-5_+|ym8juBv9I;h**-(QH*9s` z0EiqBr=`NwX~cd+;r2#T?aRRY21PA5Wh7Jyn#Zdd$z7Pn{((n5t^y;bUP)*=ZU0A; z#^k%t{@yg9u%`-9a6=V0t`QO2B6~O8yH!&$secsFTHreJcg)RS<0<1RVa5Xp6^`d+ zf{4oBnqIX+m6FA%XisgmF`>?d-FpqqWH=XpygcM&p!+&0bKrh(&>^ved#60k+HWbm zUdnL4dIT4~MvqjwRlj79F6vD9@PC504u+^kUd5n8a&bQx?2=MpD9^UB7{2Zxf=EB4 z{S>pQ1xyYbQ!0a(D+x=Xr|)bq53LE6j79G$FUU6TYK=1}bavgF1&StM4UvXjUNATo z0t+onB0JFuouCH{)mI#zDjb>xMoqpwbbj5LmDR$#w{m5E&*+tFc^=Ca03j2CKhoV; z%?H*>7Q)ZZpQD~leldx5!l5%~rp$p~5Wjv^W1x$frB3&E_+`RWZB z)KX`3xPZ5F+ORJAmalg-*@by!=dwT%=6y(~N+++yDQoae*XHWyNE0kc$P%A1E(VkO0()Q{xDV)}Gk%A@VLdMV z1+#@D7OVw`FXuElMIbHcT^%prc}wF#L$JD%M`py=|(i#~x|M zmVNSUZf?>d-x^xV>r`7r{h{@G*2O8&UQ~`?wwodI(kEuP?>~`$=j=LHcxJ6@8|D*2 zS*aIhdY3hsh39zp&E3bthOi^x%(^9@mLOoj;mDPTc#?7(mUA~X8|{0Z=I z=l)3BTkaZ6rtJAQb66s&C?tpuW=(yA!+fVI1!b?(up;rn(n$dOSh(AZ+)`3mMfdd< zemKaD9Q4FS{(HTrGZ{@H0pu5z4H%br%f&VoazTeHqslcR{cq@p=oocF%nlmu2YCED z2QDDBCz=;=KgH4WOH=L=GaVmN-1*)@lj;Q4}> zfF)Ff%wN2@`CtB_%j-~xNkaU3XZioN6~cM^Jrfai@6~|msI((cDIA(B>jck& zT^MjkPKvi67@jg9E@Cj~8_*wDP-*aaL>AxfG ziIs(ZK6*NhcaN0v5>Rc4!d1`Or#rq+F8iU9gW1_L-1HF}m0%h-{oxuFTyqudUo)xr zWmYj1_^B1vmNf~4+`hBo4?9U#Bvw;qap$V~)Z|iFWA-gg&1(-;pZYZVKMj??;D(AEJ7# z%VoIVpH;wL>qxnKFTL}`+ddo`kE!qBJsA4F`kiW2EL*%UWjdZCSJXc6SUc%Szm<;0 zv%r`TTIVT#q&KZ z%d>Y$Z*@8o{$=~o$^DZ7-*w<*i(>@Vf`5FVa)6GcenrGJwZu-aFl1mNBolH!JMPbZ?8_7Kq$&BIqVn$=o>eZhD*-Sfr%zRae0)9U0{PPOs(L ziU@*<_pl5{9_1j>E1{U8w#GgB_qNY1yemKOXGY=Cmec&j|7spTm5DaJBI~z(1>n?&oaYSpG2r z;%x-yCYpo6beN)nFiI}y0%pNb>#L6(@HS$=Y^Bl^Kkm9|)1R~z)r%_v2%PqH{P$bN zQb1tHuuq?w4O@!OJlVZN1ejk5*=`s+2oD$M<-(2JTSoEN(;b5JX%SA$!v zV}V8?9Ksug|2%IcPf0+U0577y{ZKceZwRStR^A`T>wJrp7%2vkba;haj#<)N2%qA ze%)_%ucn1s=L)wInGSIIPrtuSPx!|h_`f%peX42bq7#e#tQQ4VQC7gtQkbY7sLEg#uMK#L94};II zf$qq1DRA;iXrAWRPv5D?u@F1De2yB4tAA4reM#l>E7rIZNc@Op=xW$d{ah{v;zvX4-7CsFd(SU*yVQTY=of`;FD9Bw1Ss$xdf&WCW4BqkSuZZ=rAo z?y;??K+_^<3(Ksb*@&=RkMiFJ9XR2wo5>!Ddh?7mBGf@Dx<5qekrO z%1bm?bQ({!0?iGO-vmYoYq|-!PC=l?Dw#1r_4D>70%c*Cki}!Kn0V>s z!O8;m<_ps1hC2 z_V~_(1Q@*ry~xPtSMoa+;z$~hwLw%`o#c8XCoI09uX4(z>Mz1Z+Kf%mA)J{G?bj&$Z8TQgLE8YOzygm67%X75(>HRYdxamDdf>K)t+$KS(fG*wzElx7 ztr4@BE?5C;+qg-=d^=1axvEI-aJCDpFf^&vg)$bTjYQD|FPC*0%*f%TXWjArnu2e+ z_j?Mp%jm%*20RUVzDdq{UwP?qa`Q}`6k~9BX*gi!PAa?&Fxye7^D&Z@Z6h{+pX>YV zqlM@o&P-7j@?|4zafV^>%*;ZfsxPU7blS!*-=8dlzA>@ULs8pbkM~JzR8250D`bq0 zE)q({76nz?9; z!UV#$_jWj{7Zu+X#^O`Xdd~G@9Ke0-8Z~)H56dRB2z%1}$)+n~Ryz)cl>27_E24XP z0e{c=fpf~Y@rg@6`lNr-7r&TavYw*Sjy*-xU0$$^UEDN#X4rr3Ua6!T?h zHg)f3ysyH&d;4oyVpoQ0Htr8!o;!vaFggD+`PrbpB{qh)PptSEf=P##R575c`Z? zB%7>&>C}fSXn)Lb$)lHxF20~C0Q^kVbs3+Q;m~(jXUXL6VQ5<)nyn_Yox1^-f1>yl zgkQ}3+EyF@_fU;u?v^E@>wg2uKC0`&qf0Lfk=Fx*;n}ZL#yif*EF!$~xe0x_LtkF8 zUm|+wx0<&n`_#ySd{w2DRmhaj!@w<0rrSo((8UI8X9sjn5>6Vd9K%^)8>KR<$~mV1 zgYx8#N|^H@MVYM{&9Dw%MdNzs=`RZc8qy|gAUHh6S!p@T?{dG^cZE!1sN&JU3rJ!R zjOamjaCdUUdtO|>TcOk;D!pr2JaV`%`m1(F_f+~t7w0#V3%BmR^BWBH^GG>`pD&sC zsuQ-TKc&3m)a`YoK5PNxPCHa95pzz@f9ug}exHe$;c=Gqsnd2kMZ3QQwsVlfA%s6uexN_IuB=V|2iDm&n|rLrrS zrR-E417q?RKnsLwt(H+vpq(qZkV|oulh*bH5wOlRmO7=lX!-O%k$fKa_@!1u)Or!8 z-J!Vh3=kR!i>O7Wny`7dJ$N+5RyULoEdX`?P|yhy@(F4xW2o!seJ)T0^ktnT3~G}S zKzlnI3^o=<;2SFiYoF#5$(*b&SfXD`)eE%GmC>Fw#}p2;YWdz+q<5L}^Rxc=5fSxL zzeq1}g!*{!U&ZMOc4;uccn-G>imDi&d}(Gp9wg&@JEI1n5~n znVGa)Bz8zqWLY28T9k}d3j>lkwcQn_KVv1SraJ`f29AdEBmD5sc9bE4j~E)9%plvQ zrtn}x#bHCF6VEDFJN};t4*6^Q9h)*NY&S`#sIf{6$)ZK~@CDZKH2MBljG+a+yFs}k zxc=`G9|Y?&*Ij_3*YQ=AmtBAJT+_)Z-Q&ZKR<{i6P*@)J+ahYCyV)yoTxZEMS!(^0 z*O{(oa|xEoX6R8nem&bOR2w&pZM%Ko&?jswn&*s94v|j!wP!#KPl>kDO~|WF^x&tH z2FZx$hJT@!D1vKKv>`lP$u!*ooLYG$?d;hRCbsjVi~T&iSfeJ_$&QBh5W3lHZa5o3 zO&9QQBheEy;lfDws*(bvU{bHkFNohGCMwpAn7c_4Ut;rH52xbJp0&bd?TY0~`(EAr zD-FpMh|QV3$TK1l?I&g_D>|GJ!Yu|__)~zr0o#wdZc^u%sqvE~n^Tt95hS+XIar5e z6sLxm^eazdu^3};6C9XKZ@_rNfsP*`zqs7X5_Xpp^@RY* zS-3@ZUxN~UC-L*DcDCpccR4KY_f!h1O$6p1q}uL6i;tFw+60&bg{|m761=Wn#M$fm zLj$S7=prUM0SC;^!M}9{VGqGybJ`-EsU&Z1{Y^2CNQ4lRc-2sn!b36<4c}zfKiYx) zmmC5YBJkEeH5)2GNnu5TGNY7?p`r0TYy*Z{ZB+1 zxC}|lbD?$i2n!>z(Ni9?4gkBND~9`r93$O{h0UBwg1ij+!`uyhh_l*m;{TW75pUf9 z8qr-Xk|%gB1qGA%6fOzX2jB_|pRbQ2sQAvOEmPrOo=?~bB`aC+@e7c6q^8z}1h3m! zutu)V-5>RS8H|~;1VhBcu$A{}f<39;Y)NNsB-^*Ih__($2X)xQs{=5Bb^c4xb>Kd} zwa&8q^N?Y#Jbvzm6Lj|N9=+UaFdN>=f#S~UBna5ciaepKtF7x_r8+u~a^7X!hS!_3 z$d3$;|0nVg&xBb-Sf1TaMG2mDIL2e zifyX=4FLq~aZ!N^q8jG5uWT#Sj4q>foy8rA({;Vh;UT4GC0h9TRgX+jy(%a4#azFw zBC;)ri^yVcDd3k-KByeq?!|w>io6%vBHA_6j?r5qWPzwtI6aj-QBO!${rqprJ6RP4 zZ)cwwRg-GnsNcS&Mh~%IrTFzGVPT z^7m;ZG*2-Pf@ilf7x&Y5joFO6Vxo_y4kS!jZp4s_1@t;%mwZv~#_gd$K9zfFB6wKiZ1v>o%2XkCAAly-=7j@6Dj1yqJv%=zq#}2m z47NP*^Uq(yzn6cPxZ9FD$&*j@hxR()28z&0;-{>pE?AZYfSt4wwnGBGtT(XMz({-} z80IMsfO3ht7^OCUg>5V4R>Dh{-B0i0dR$hdclSK+!LN5cRPp40<4_}odEE}ftro}o zS9V%KiFcvjr_`f3ADoUp6}w!pnxC`WQa5kseH-)9&}~jl;jyU5&)gPigf-`UFH4(B zvo(12wUG8R{9oxeMK@^ zzh^P?RYV-qDl7?sJ*&V5V-~bnzrhwGmQbVQhle`=XG>C z$F-cyEkg#%>BKnzm^u4vh8XDl?Dn4 zp1UZLQUv*jryh+MN`{r$v(*qEawBQ3Q{MdRuAm7O)hx6K3Q@6V)x3n>8HMrK;9_jMELf1H4sus{4W}T(@=6m z+t+TV+FQ#$KYL$vIzR9pKcB?@*;2WyK-NHW-XFWX`yQYUUW{;<^wN(DPFkm$4CO0S zo*zUJP4NCMp0$5R_>!f&#?sq($mqPfxfh|HN|y@u%!X!%uydz-yL1)OC3>ywSzqT8 zVnx{r_|HnkMH>$F!p>3wAAk6v?{kp=F%+TBN5yx_0~B7Y)s5FEbZ6lmx5S~Ay3-0g z8}F2kM&I~q`|$Y1Q|2?8j9fVLWisOY!gf8uW3o23hkF24 z9~j&)7jw0L@@KdjP#DV8x83|qE#LwBaz+5t zEKT3-02nRk6XNe-;1|hJelsX~4uHS-eAK8`P62DLp$sTd`8G8UJWh0m5r}yF0&E?2 z8Pgq$Idv>XumQhGKm+rcKH$VCn4!DCpt_D26tU39GC^Hxb%<u#oSnX<`Osi z0SxW(yR>xq%XNMF_2KD$-reFCiy_onH)|{axmZ9z^9HaaEs^IH?}8-wt9&7?XrW-mRJc!YDy7wi&{8H}__UrEx$66ZpPM`34M?P-+0_`DBJi!8+{QJoJ?A zR4sJ&1@Dfu;{-n3VNtuwDTV?J&ZlFy}eS#p~tT?*{A+s|8N7FgooFIk^=JumoB-sez$! zg(Ze&*cCV4PU+N_p6(%JeQak`gRuhb$p~V48#qvxw@Y{b@{ZS;o>LL_-qYL=?Y0yj z@B;fFi{G6j)ye%MjjI;8Eu@7aBlswqSClO7&4`>#$K9q|!>jbD-obE;Hrl(Q>*hPl zASWkhh4A#pTkhW%Z`l7StjKlad1Dhk?{j-Wja1I!gAi{LBJIB0# zAodFkY7GPO^^61kiDp&22eDkKmlVXBx#V0lUu-OOo-_ss0kU^+ZWf01xyt+E_|dOT z`dabY2^kYfxQvJOQio$dJTFk{2#(mRjj4KhD~yIm01rMAXOYfbCH;sN9*5iQS-0*v zZ&3HNO~Ku%%LwePkgbzR+)6VOBE!kNW#aRp3}?`c=#rn9U5c0qV;D8KpZbxH$T_;w zp)6dhR!hO%E-9oFiQ1A0&({+woP!$>0ZF%BJ(q-Tu!2bwB8f zJ&%h@q8==s^t<$jcO@>T(75~wEkaDuNchaMT6-nE3z z!+-=fI+exuk0-APl~NOi?>6Jz%i>Rc(eS*vL#)sg1j2+{6~Ne)hh$Tlc5Gmw@r}NR z?@}5nl|Bz<)H@PUzcs=s(yFb0`+j4gWo9YvsS?0Lr2)ka_ViGz(6?s(hMpiEO%19q z8%pn4=?MnqT20mNq3%r?{>{SOjx%N{OL}>AcNR_r`m9&1>412~-aV95b!f}P+=gW) zHbyQHHS)8iJct!nd4H{0QDL=ki!F)15eXT6JuOKY5bWe&l2%t1?x01Zay!Wustpn{ z-u|7EKM$CN1HA-~$GdwMv*(g;`(5n4lB%lfh7SN6u=Cd*9}Em=KN>{=pqY?&Yx_EP zjmp`thD-FeUyCFVq!DwrXIouY5=KfxS6|IO^&WxfH;7gVtEkjTHxv3WXx0{1HXvO2 z)K~X|2mB&u$5MfAH{lnMge{ubJ^NYnyf#eMB6W&4kg6?<0l@ca-R{6o2 zVme?$>cNDhuYvtKnAI=_OFYO9--Rj}B};C$;falmQ2Yso@4p>-p)&5S7mSUm38#-m zR1XvR;#cBXdt|a#}OBjU*^K+Hr6^kWDg>t_Q4IO)p*A#VLe2Am+bRFVy~IP)WT8s-s*2FQSBP z#>f#%zk@?P$%-;S9tyeZuu=Q2SBLU~a<|H<)gp#}5Ol|%XUlf9XXp>)8K<7zS*LD~ z9^IRv#hPu4;L*CB;_f-6;1(_&4&htZj>+}KPS7J^HkC&w!5(|8ltJfOv1G6^^E_@2 zTtvZ>TD7NusRC??a_${;{00FC7a6LHS2i*9g7$4UgpM+o4~_JKz99Rxds}rOu z`+23H)dpEe0xk~dpYb7J??JxWJwvnqlvXG_L7V+w6v z|0?`^q7&HZE4_%Wu5XS)SF!lMvNQ57J(t7%HSLdICE+Ms=7Jdf-WKIAb0TAN_~O)J zP?1{Cpc5uo#X5fxi+%>O{8on)FDAc-o3UpL0CmZg#=H)l5h&u$(!YfRlGDU<@-%=z z>=i_@5&+l{EnK|o3nZGBx>(kiF?{t5gHeI=(&dZ}4uw;12vAOzK9Uxj=`%ht4_ z^dqAKpDo+NA+n?9FpZ1p5ME2M$^J`G3Vo!eiXsBim4C+~RuP%B#{C*qy}pg#*N}0&0XcP?l~#bb zU@j`wYW7L9&O3iqx> z4TBHZ$wvsWS@Om3=+|xxi;1dj_nT)Y#5j^?t0S8Ff$_i=jLDT&o1}>Ll0#dbBM0&) z@`kYQY^1k_kJUV}6+=*{%9%6x=ZjaS*OBFpe)j#X(6giw&U~g}q2b?GGawNv7o8my z?yA(}j&&IMwi!1$lp|5}=L?z(!yf$CwLvv50R$%Qp43Wim@_ZnhPV)CeAT|*Hg$pd z_VG?ni9bbWH5@imqzY%;zLvN0AC6D|-h()rrl7UM$nIz6%@%l+5V=QZ(nK8_m)+H) z@c3X)XHIIP|Gahfal06yJI389<7kRJWMaDiCj|w|bb6gl;4}8tgR=(&#VPqKHZ zhfEu@$XPfmq(9bjF~Ho+gU9g>1)l?w-Npp&r(9|UU0Ff$ zVLRxY`I|Ew5na?_=~bA{7rAZL$qnw9!i>p<&>g$@jc_acm8FvODz+79Jt_#yA&ZMFZn&9 z8h@hVg?ffC+kz=u2*BN*i%oqxr5VYTjeo3Wb?)whWZtpdfW+bY{<-E}G*|d2c_r?Y zclPtrs>@|XT0X7IG%11?Ie~zwt&9rFHO^gf!JqO6iFnXmJ!}R18EF+kIO3YbchrbF zoZJ<~{zUA-fwET}5bxXJnOY{hbF0)l7N)=IX#Ge^7a-sCEmPv3R_ctxmgoec?v@JU z3mg;Os5>~R^6}+eb_GIRN=1JfHY09GV1Zx1T#2f52B*Q!RJ7f07?c17Gj|DbQ zKKPSJ*(2Wk)D)s_%ziTNHhbxcZN0}tvmw2Q^~KE8$$3U3tm|`15o;QPta2PUIozbPy`T;t@-wpJ%HjYQ^&ep!DHQt!$GsbVW2;|sR-dNU$r z^U`8r-N1(0kHDGF#KS<52?Vb!{G55!rr(0>AFAO3!uU03HIibb#maUTwSxgAJ76r| z^~zn}uNe;hJWfXBv%nz!e-(_F3@D!M*CaP=t;ewn{!*t!Jx_>c*Sc)T3;L5T^=fhF zkpG#g&vns_*R(ldH$tt)yu&J2hdqu-&mRIxnt^8lBI79afG>2sb+wLm?l*X$iPP-73GT_P&t1t> zLAqLp4Rc*)LbDsof#)xL=^(k5utI)f)U9~!A(3!wM+w8k+Wt4^A#8RG!YFG~7qo9l zW;q7cS3Zuoa49zyph+l0$liD+K9Kz)(>S{X`!Xe>sygh%V%#2w1R)@zOId;@*NBN`?R=;R@tFp6%Lea*OmsWs18Vp&7%EdvRfP4xvg5Q@bu1Mb##I7QFWS0CB zOsGj7TF#SwjLed8W3iQh$I{qlg%v${0OVUbaX0*aSlxR396AROK@s!**oM?)Bkin& zzIr!^U`|;jj!GixZ;zk@IsJ9TVPU4}KE z4|`YDGTZHP?RU6Jm(rDb^~|ryroN-ZZK7E0!AsA+`6Mp6?B;z~kkKi08d(#@9`eqT z&}F+Tlope5@8NXx|GK1@sVyk+#>Aduh{b3zmRn+*`qW5%Lb9?UbAl1N44%8?iDL}_ zf2MR>g7%(1|BxN>#C<{k;|!U$!Nu{XG?#|W05A7I6dd)54z5a$Q}lEh0QC&#a`qZ#x|B zOZk4kYM>_p3HaB|v)fz^0J}=D>$eQ|M)^YvndakfM;A+G{%C08Ew8+DmOC2P6}f9; zQ`<~y^~MS&yGyU{7304ffC66e&X(7{;JA`S@w;F4B=;F5|6_uPFo8Z=yD3t}zViP# zLTjnExKGkys8J8m>|w`PMHnZpmQNGU#^7)6NbCMX68uc(Rxm;L`jea2!5a6=1hYyO zcESTP<&8NoSy}2UUZdWs3;$HUq;-ibDQ{vrFx<)rq73=x1@RWLG$7Hp4@_5bJw9B# z$P0o&oowI)H}-0aDkfTgW6C(Lv7qx$rC`R=w_8KykIVI{-!A&lRz?#VhX zAfup&NZ%V@E|ggO#|{v#K}RyHfF};DL@{LXnQH&!uXG6;gp=N{WBX02zb6;}5>_6e z`lKZgg%98BVbZH}tbeonkW$f3gL41T#>*|FhUV6=jHk!WSH;?ORM133S}+VnP(Rc zzHNN)!oHm}5@mC~DJ-!Dm_yKH28!Ekf!Q~Wmp3L#e~HnfPKv4==85{_+_YHd3+B>w zzklTAm_=Rx)mIGx$Gnm!^3@Kf>*V#wOztH*4wMdUT+5EIC3+UJk(i8wEb*3Ef83vI zJM1ZcAwS){64KmE8skiH?o?6!(z^VBwePfD2()K-ZPc%UG_G~^_n?v!CI?+d7u$97 zM}c#XdtB;Rx~#!^{e;VloV?_+s&3Z{dHmfN{W@@89PsaL|E%XXZipx}k2ED@`ya3p z+8jUCi9!}8DEF0kUyu%1hUDkod1=UE1*iAU`C3(f%?@@aWv+7FnkAok z4Lc316G(fRI3V!2E@X0IWF?TUNQ?Hi@Yx-nF1^NQ(P4J$0hGs$A6wQ#C`-36dt)NQ z%VK!2-gxzIHUS(d+Uvu7HLSV8HddI^&#TljGJ~#CKr-Ix|0qDVIk%da6>0m|_f9S5 zA4TP&jUe&z#wF{Bq5~DO)*@0=+Ci>gc)4yKp_mUG!5wP4_w3EDa@A==8^I5N*4RTJ zVG@U}fms>VWF5Sf19#i|L>#rc&h>?w#pK3~64azFb>3rlvrH1@?xGdt(O&rcEP2!jG(L?ey^2PENXMzxezIk(DR!z4O+s%BpihC(Wh@f~q-8RJ}mTa~KZS)(K5; zA~(|w6xfg#+y|k0e|OI_-(TY@yIM;t-?xPZDyV<^&2}!eVWCCxBPl=xi5rFF*rwdF zf|Vfh^d!x)SxyVjM(dfpfkW2O#b`08Ic3u~6t9kN)xL@`C;Q{OYS3ObxEKs{6RXDBAYC68b#7SkyMxO!U9*NY{D zm()7w{{X|a>-UOx8J`jAe)tdiXdhB`BuldHs`{*}cwz63*fTc31Z8rG>fbPKU54OS zW%u<=^LsnGl74HJvFVLg(wdez)S&Ae6!?HVG^fNFl1~h&Jy=|7_Lj0+^vb8BMV;T# z-u#H_d89MxtX>NWY(k{z6vk}qOU3rxN(O|Ws7A<08Qv}^E_&w6PKOCjpoeN^qe+Uy zl;tt$>-*miT9c5t%(fD{F(!ye4!V^x)4X-p4#bW36?z4pI#p5@hc=539=WtVA0rdZ zH2Pp9N`83-%n2M1SU{J$Z8vK7G3J$1XaE*R_Yb{+zbGN7ac7&pw}h7my4G`kIVhW{ zHwu8B!4r73yS|!?0!(&jy}>wAC$sb$6scTT*2ED493Sbwng zc_y-e&oBGN6`fhUZCqn(44Ca>m* zy945jcMKm8o`HGkP`*5+sy!QyOOWn(dJfO6U=)o(*f{=dR{FMX0AWe;pFdUn%*08) z(L}}K6l0@gyO2cVF*mNkI!OLXq%+$ILyGOCq1vr>CqueorjmQD zDT4r)Zv7;!{B;MR3Jfv)C6{G!0zEW(_j$Z~50S%RH8(%MryC=PMei#~8~b9F9a9r= zz|Zg8$h1qxrIV}J&bFG(i))394&hQlOcT>9FEvRW9kz+Ir{KLn0;hRyXE$$J-17M( zA^ia<-#A%s(=vKaVpCf?9Cb27HO`9DdBoZHy(ad{3cVcTP<%ZP=m%nH#Hm~GSuB3R zZ`e7gh{-oXhAo_|_WnpODJ6&8ux;Gyoerl15L4k(S3tayuZG)m{zj#7OGUHyfZx$S fy>ze^Wf4RiUyeqe(+#_x(%@zH#RQZ7@8tgh`(PJ4 diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml deleted file mode 100644 index 34b8202..0000000 --- a/app/src/main/res/drawable/ic_info_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_release_background.xml b/app/src/main/res/drawable/ic_launcher_release_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_release_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml deleted file mode 100644 index e3400cf..0000000 --- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml deleted file mode 100644 index 2aef437..0000000 --- a/app/src/main/res/drawable/ic_sync_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/img_i_full.png b/app/src/main/res/drawable/img_i_full.png deleted file mode 100644 index 97c195c5e60666f8d9320a6bc10561f4c1b284ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1370 zcmZ{kYc$k(7{`DAnQf>7`MzQhs4s*xYia{Wmp+GgV|}%zSw=S-*djt=RD6j-#5>5&XeQe?yP`BBLM&k zt}et=64Jk079pt-mQ@4^U?FbKL?F(}AMBEJa7tsqYF}66rU4fjvKQZ@Pn7#r8LB!Fn}c?MGt`0weGI2PupF&DkNIeYB5K9x)faBe>);m3?<8;dcNCW=rF70trQnk0U+q&; z`!4U6xT3C7vmiTM17tK9B%8KqU)&yX&9HD&g%o#HF)}x>u^-o>Bs^x-a{(RNo{-KL z+NE-y){jLc`Yk+;5N{(y8_J&>hE5OcpsyS8ySkKeedTFW19;c;;Iitg+bRPyz<4t+ z-HC)SZct=)khPw+(10-e!G@Y*x3TBf`S%dbUG@Ba9Uzu$&@ERch^T6N&IV-UJ`gcw z=A1pLNpqjSa*ql|F>ys&HEVAOD;<{fQC{m3pxqh`5XQuJB-HuI;#W&{WNO#Oh$|-_ zZ1?i4FBp8w-gv^lH<$it%N|1^j~fh<)(poPgT2B=nh9?8AdjpNehJ=HskJao4|DST zM^(&gV_|#DWGh`Jop+Tn0tW2r~oZMSO555Nd`( zJ7FBn3SUI4Y71MpKqJ2TBA^V>#}TiyG(N-Ep$|dOBCMdW+X`L-9;+S4Gcr0>cYZ(P z4-MaiCZ))CO~FZEZ6)`cQ~?vI1W|IT*#eoHP`P65!CK7%W<0$HV9x_Tu+$C%+Nxu~ zmy~IfZIroveDYjE5g~d%U7fD&O>dK_Xw>?!cPlCB)w1Bq2=84gcM|T-W9YMqM#Gh^ z(uzJD@r^_l+AWBQ;r#?@KcQ_$>40Wrh?${OEQ*Dvo$jl$0W0DQHp4eHR(-jjETX;i zW}A%`RqXNiuxCW2i)Fl-E$|fy4pdv8f}8npb}oJyM0E$kz!C8-yRoQ;z5F|7bTI;y z*O(7Yvi+5lx%^@~Aa*wFM^#^M{JAn-f3`{Z2tMa=%bXQYbDtvd>I{LQTSXRgK5b?d zK2x&Cd*0;x?5Y%aDR{=MXEO`~Cz*P0qA@^DN0%aj7P!mfZwIwyxuHahUY231L3ti{AaL zIS%=@_?qDCLYWSDQSUhaVl!efYhhiF8uxNw3*=wt9L8Cgn_`dPu-N=Uc;0^kQ8A$r;mQ9W*y(S_N&>*u(VbXl HA9VRIdL34z diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index dfa3dfc..0000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 681d46f..e9fd9c1 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -23,4 +23,9 @@ android:src="@drawable/add" app:backgroundTint="@color/colorPrimary" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 077fa23..7120641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,9 @@ Farmr - Settings Shifts Add Shift Edit Shift - Delete Shift failed to insert Shift Shift successfully added Update Failed @@ -25,11 +23,6 @@ General - Enable social recommendations - Recommendations for people to contact - based on your message history - - Display name John Smith diff --git a/build.gradle b/build.gradle index f56abf1..4e3983e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.7.10' } repositories { google() From 07d7e6cbe730ef6a780cef8e371c157a791d6ac2 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Mon, 28 Aug 2023 21:13:43 +0100 Subject: [PATCH 08/15] - mid commit --- app/build.gradle | 2 +- .../appttude/h_mal/farmr/data/ui/BaseTest.kt | 91 +++++ .../h_mal/farmr/data/ui/BaseTestRobot.kt | 161 ++++++++ .../data/ui/robots/AddItemScreenRobot.kt | 41 ++ .../farmr/data/ui/robots/FilterScreenRobot.kt | 29 ++ .../farmr/data/ui/robots/HomeScreenRobot.kt | 26 ++ .../data/ui/robots/ViewItemScreenRobot.kt | 33 ++ .../h_mal/farmr/data/ui/tests/ShiftTests.kt | 77 ++++ .../h_mal/farmr/data/ui/utils/Constants.kt | 1 + .../farmr/data/ui/utils/EspressoHelper.kt | 123 ++++++ .../h_mal/farmr/data/ui/utils/TestUtils.kt | 32 ++ .../appttude/h_mal/farmr/base/BaseActivity.kt | 18 +- .../appttude/h_mal/farmr/base/BaseFragment.kt | 17 +- .../farmr/data/prefs/PreferencesProvider.kt | 12 +- .../h_mal/farmr/ui/FilterDataFragment.kt | 46 ++- .../h_mal/farmr/ui/FragmentAddItem.kt | 4 +- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 3 +- .../h_mal/farmr/ui/FurtherInfoFragment.kt | 85 ++-- .../appttude/h_mal/farmr/ui/MainActivity.kt | 2 +- .../appttude/h_mal/farmr/utils/Formatting.kt | 10 + .../viewmodel/ApplicationViewModelFactory.kt | 3 + .../h_mal/farmr/viewmodel/FilterViewModel.kt | 21 + .../h_mal/farmr/viewmodel/InfoViewModel.kt | 40 ++ .../h_mal/farmr/viewmodel/MainViewModel.kt | 371 +----------------- .../h_mal/farmr/viewmodel/ShiftViewModel.kt | 52 +++ .../farmr/viewmodel/SubmissionViewModel.kt | 308 +++++++++++++++ .../farmr/viewmodel/MainViewModelTest.kt | 239 +++++++++++ .../farmr/viewmodel/ShiftViewModelTest.kt | 171 ++++++++ .../viewmodel/SubmissionViewModelTest.kt | 85 ++++ 29 files changed, 1646 insertions(+), 457 deletions(-) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index d030c25..86f0e87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,7 +44,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' / * mockito and livedata testing * / testImplementation 'org.mockito:mockito-inline:2.13.0' - implementation 'androidx.arch.core:core-testing:2.1.0' + testImplementation 'androidx.arch.core:core-testing:2.1.0' / * MockK * / def mockk_ver = "1.10.5" testImplementation "io.mockk:mockk:$mockk_ver" diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt new file mode 100644 index 0000000..87e8ed5 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt @@ -0,0 +1,91 @@ +package com.appttude.h_mal.farmr.data.ui + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import com.appttude.h_mal.farmr.di.ShiftApplication +import kotlinx.coroutines.runBlocking +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Rule + +@Suppress("EmptyMethod") +open class BaseTest( + private val activity: Class, + private val intentBundle: Bundle? = null, +) { + + lateinit var scenario: ActivityScenario + private lateinit var testApp: ShiftApplication + private lateinit var testActivity: Activity + private lateinit var decorView: View + + @get:Rule + var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Before + fun setUp() { + val startIntent = + Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity) + if (intentBundle != null) { + startIntent.replaceExtras(intentBundle) + } + + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ShiftApplication + runBlocking { + beforeLaunch() + } + + scenario = ActivityScenario.launch(startIntent) + scenario.onActivity { + decorView = it.window.decorView + testActivity = it + } + afterLaunch() + } + + fun getActivity() = testActivity + + @After + fun tearDown() { + testFinished() + } + + open fun beforeLaunch() {} + open fun afterLaunch() {} + open fun testFinished() {} + + fun waitFor(delay: Long) { + Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction { + override fun getConstraints(): Matcher = ViewMatchers.isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + }) + } + + @Suppress("DEPRECATION") + fun checkToastMessage(message: String) { + Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView))) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + waitFor(3500) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt new file mode 100644 index 0000000..d864acb --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt @@ -0,0 +1,161 @@ +package com.appttude.h_mal.farmr.data.ui + +import android.content.res.Resources +import android.widget.DatePicker +import android.widget.TimePicker +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.PickerActions +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.* +import com.appttude.h_mal.farmr.data.ui.utils.EspressoHelper.waitForView +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.anything +import org.hamcrest.CoreMatchers.equalTo + +@SuppressWarnings("unused") +open class BaseTestRobot { + + fun fillEditText(resId: Int, text: String?): ViewInteraction = + onView(withId(resId)).perform( + ViewActions.replaceText(text), + ViewActions.closeSoftKeyboard() + ) + + fun clickButton(resId: Int): ViewInteraction = + onView((withId(resId))).perform(click()) + +// fun clickMenu(menuId: Int): ViewInteraction = onView() + + fun matchView(resId: Int): ViewInteraction = onView(withId(resId)) + + fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId)) + + fun matchDisplayed(resId: Int): ViewInteraction = matchView(resId).check(matches(isDisplayed())) + + fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction + .check(matches(withText(text))) + + fun matchText(viewId: Int, textId: Int): ViewInteraction = onView(withId(viewId)) + .check(matches(withText(textId))) + + fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) + + fun clickListItem(listRes: Int, position: Int) { + onData(anything()) + .inAdapterView(allOf(withId(listRes))) + .atPosition(position).perform(click()) + } + + fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ) + ) + } + + fun scrollToRecyclerItem( + recyclerId: Int, + resIdForString: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(resIdForString)) + ) + ) + } + + fun scrollToRecyclerItemByPosition( + recyclerId: Int, + position: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position) + ) + } + + fun clickViewInRecycler(recyclerId: Int, text: String) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click()) + ) + } + + fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem( + hasDescendant(withText(resIdForString)), + click() + ) + ) + } + + fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, click()) + ) + } + + fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) { + scrollToRecyclerItem(recyclerId, text) + ?.perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem( + withChild(withText(text)), click() + ) + ) + } + + fun swipeDown(resId: Int): ViewInteraction = + onView(withId(resId)).perform(swipeDown()) + + fun getStringFromResource(@StringRes resId: Int): String = + Resources.getSystem().getString(resId) + + fun pullToRefresh(resId: Int) { + onView(allOf(withId(resId), isDisplayed())).perform(swipeDown()) + } + + fun selectDateInPicker(year: Int, month: Int, day: Int) { + onView(withClassName(equalTo(DatePicker::class.java.name))).perform( + PickerActions.setDate( + year, + month, + day + ) + ) + } + + fun selectTextInSpinner(id: Int, text:String) { + clickButton(id) + onView(withSpinnerText(text)).perform(click()) + } + + fun selectTimeInPicker(hours: Int, minutes: Int) { + onView(withClassName(equalTo(TimePicker::class.java.name))).perform( + PickerActions.setTime( + hours, minutes + ) + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt new file mode 100644 index 0000000..f2149b2 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt @@ -0,0 +1,41 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() } +class AddItemScreenRobot : BaseTestRobot() { + + fun clickShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> clickButton(R.id.hourly) + ShiftType.PIECE -> clickButton(R.id.piecerate) + } + } + + fun setDescription(text: String?) = fillEditText(R.id.locationEditText, text) + fun setDate(year: Int, month: Int, day: Int) { + clickButton(R.id.dateEditText) + selectDateInPicker(year, month, day) + } + + fun setTimeIn(hour: Int, minutes: Int) { + clickButton(R.id.timeInEditText) + selectTimeInPicker(hour, minutes) + } + + fun setTimeOut(hour: Int, minutes: Int) { + clickButton(R.id.timeOutEditText) + selectTimeInPicker(hour, minutes) + } + + fun setBreakTime(mins: Int) = fillEditText(R.id.breakEditText, mins.toString()) + fun setUnits(units: Float) = fillEditText(R.id.unitET, units.toString()) + fun setRateOfPay(rateOfPay: Float) = fillEditText(R.id.payrateET, rateOfPay.toString()) + fun submit() = clickButton(R.id.submit) + + fun assertTotalPay(pay: String) = matchText(R.id.totalpayval, pay) + fun assertDuration(duration: String) = matchText(R.id.ShiftDuration, duration) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt new file mode 100644 index 0000000..22e36ed --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt @@ -0,0 +1,29 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } +class FilterScreenRobot : BaseTestRobot() { + + fun setDescription(text: String?) = fillEditText(R.id.filterLocationEditText, text) + + fun setDateIn(year: Int, month: Int, day: Int) { + clickButton(R.id.fromdateInEditText) + selectDateInPicker(year, month, day) + } + + fun setDateOut(year: Int, month: Int, day: Int) { + clickButton(R.id.filterDateOutEditText) + selectDateInPicker(year, month, day) + } + + fun setType(type: ShiftType?) = when(type) { + ShiftType.HOURLY -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + ShiftType.PIECE -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + null -> selectTextInSpinner(R.id.TypeFilterEditText, "") + } + fun submit() = clickButton(R.id.submitFiltered) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt new file mode 100644 index 0000000..4f5c5e6 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt @@ -0,0 +1,26 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot + +fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } +class HomeScreenRobot : BaseTestRobot() { + + fun clickOnItem(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position) + fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) + fun clickOnEdit(position: Int) = clickViewInRecycler(R.id.list_item_view, R.id.imageView) + fun clickFab() = clickButton(R.id.fab1) + fun clickOnInfo() = clickButton(R.id.action_favorite) +// fun clearFilter() = +// fun applySort() = + + +// fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location) +// fun refresh() = pullToRefresh(R.id.swipe_refresh) +// fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4) +// fun verifyUnableToRetrieve() { +// matchText(R.id.header_text, R.string.retrieve_warning) +// matchText(R.id.body_text, R.string.empty_retrieve_warning) +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt new file mode 100644 index 0000000..9585ec9 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt @@ -0,0 +1,33 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } +class ViewItemScreenRobot : BaseTestRobot() { + + fun matchShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> matchText(R.id.details_shift, type.type) + ShiftType.PIECE -> matchText(R.id.details_shift, type.type) + } + } + + fun matchDescription(text: String) = matchText(R.id.details_desc, text) + fun matchDate(date: String) { + matchText(R.id.details_date, date) + } + + fun matchTime(timeIn: String, timeOut: String) { + matchText(R.id.details_time, "$timeIn-$timeOut") + } + + fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, mins.toString()) + fun matchUnits(units: Float) = fillEditText(R.id.details_units, units.toString()) + fun matchRateOfPay(rateOfPay: Float) = fillEditText(R.id.details_pay_rate, rateOfPay.toString()) + fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay) + fun matchDuration(duration: String) = matchText(R.id.details_duration, duration) + + fun clickEdit() = clickButton(R.id.details_edit) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt new file mode 100644 index 0000000..53997bd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt @@ -0,0 +1,77 @@ +package com.appttude.h_mal.farmr.data.ui.tests + +import com.appttude.h_mal.farmr.data.ui.BaseTest +import com.appttude.h_mal.farmr.data.ui.robots.addScreen +import com.appttude.h_mal.farmr.data.ui.robots.homeScreen +import com.appttude.h_mal.farmr.data.ui.robots.viewScreen +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.MainActivity +import org.junit.Test + +class ShiftTests: BaseTest(MainActivity::class.java) { + + // Add a shift successfully + @Test + fun test1() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.HOURLY) + setTimeIn(12,0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(10f) + assertDuration("2.0 hours") + assertTotalPay("£20.00") + submit() + } + homeScreen { + sc("This is a description") + } + } + + // Edit a shift successfully + @Test + fun test2() { + homeScreen { + clickOnItemWithText("Edit this shift") + } + addScreen { + setRateOfPay(20f) + assertDuration("2.0 hours") + assertTotalPay("£40.00") + submit() + } + homeScreen { + clickOnItemWithText("Edit this shift") + } + viewScreen { + matchDescription("Edit this shift") + matchDuration("2 Hours 0 minutes") + matchTotalPay("2.0 hours @ £20.00 per Hour\nEquals:£40.00") + } + } + + // filter the list with date from + @Test + fun test3() {} + + // filter the list with date to + @Test + fun test4() {} + + // Add a shift as piece rate + @Test + fun test5() {} + + // Validate the details screen + @Test + fun test6() {} + + // filter, sort, order and then reset + @Test + fun test7() {} +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt new file mode 100644 index 0000000..e18ec2c --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt @@ -0,0 +1 @@ +package com.appttude.h_mal.farmr.data.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt new file mode 100644 index 0000000..e008cfe --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt @@ -0,0 +1,123 @@ +package com.appttude.h_mal.farmr.data.ui.utils + +import android.os.SystemClock.sleep +import android.view.View +import android.widget.CheckBox +import android.widget.Checkable +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.util.TreeIterables +import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.isA +import org.hamcrest.Description +import org.hamcrest.Matcher + + +object EspressoHelper { + + /** + * Perform action of waiting for a certain view within a single root view + * @param viewMatcher Generic Matcher used to find our view + */ + fun searchFor(viewMatcher: Matcher): ViewAction { + + return object : ViewAction { + + override fun getConstraints(): Matcher = isRoot() + override fun getDescription(): String { + return "searching for view $this in the root view" + } + + override fun perform(uiController: UiController, view: View) { + var tries = 0 + val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view) + + // Look for the match in the tree of child views + childViews.forEach { + tries++ + if (viewMatcher.matches(it)) { + // found the view + return + } + } + + throw NoMatchingViewException.Builder() + .withRootView(view) + .withViewMatcher(viewMatcher) + .build() + } + } + } + + /** + * Performs an action to check/uncheck a checkbox + * + */ + fun setChecked(checked: Boolean): ViewAction { + return object : ViewAction { + override fun getConstraints(): BaseMatcher { + return object : BaseMatcher() { + override fun describeTo(description: Description?) {} + + override fun matches(actual: Any?): Boolean { + return isA(CheckBox::class.java).matches(actual) + } + } + } + + override fun getDescription(): String { + return "" + } + + override fun perform(uiController: UiController, view: View) { + val checkableView = view as Checkable + checkableView.isChecked = checked + } + } + } + + /** + * Perform action of implicitly waiting for a certain view. + * This differs from EspressoExtensions.searchFor in that, + * upon failure to locate an element, it will fetch a new root view + * in which to traverse searching for our @param match + * + * @param viewMatcher ViewMatcher used to find our view + */ + fun waitForView( + viewMatcher: Matcher, + waitMillis: Int = 5000, + waitMillisPerTry: Long = 100 + ): ViewInteraction { + + // Derive the max tries + val maxTries = waitMillis / waitMillisPerTry.toInt() + + var tries = 0 + + for (i in 0..maxTries) + try { + // Track the amount of times we've tried + tries++ + + // Search the root for the view + onView(isRoot()).perform(searchFor(viewMatcher)) + + // If we're here, we found our view. Now return it + return onView(viewMatcher) + + } catch (e: Exception) { + + if (tries == maxTries) { + throw e + } + sleep(waitMillisPerTry) + } + + throw Exception("Error finding a view matching $viewMatcher") + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt new file mode 100644 index 0000000..2a8c143 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt @@ -0,0 +1,32 @@ +package com.appttude.h_mal.farmr.data.ui.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt index 3960cf5..6ddc382 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt @@ -1,26 +1,10 @@ package com.appttude.h_mal.farmr.base import android.content.Intent -import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelLazy import com.appttude.h_mal.farmr.utils.displayToast -import com.appttude.h_mal.farmr.utils.getGenericClassAt -import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory -import org.kodein.di.KodeinAware -import org.kodein.di.android.kodein -import org.kodein.di.generic.instance -abstract class BaseActivity : AppCompatActivity(), KodeinAware { - - override val kodein by kodein() - private val factory by instance() - - val viewModel: V by getViewModel() - - private fun getViewModel(): Lazy = - ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, - factoryProducer = { factory } ) +abstract class BaseActivity : AppCompatActivity() { /** diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt index a939028..3a25089 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -5,11 +5,13 @@ import android.view.View import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.createViewModelLazy +import androidx.lifecycle.ViewModelLazy import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getGenericClassAt import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein import org.kodein.di.android.x.kodein import org.kodein.di.generic.instance import kotlin.properties.Delegates @@ -21,14 +23,13 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) override val kodein by kodein() private val factory by instance() - val viewModel: V by getActivityViewModel() + val viewModel: V by getViewModel() - private fun getActivityViewModel() = createViewModelLazy( - getGenericClassAt(0), - { requireActivity().viewModelStore }, - { factory }) + private fun getViewModel(): Lazy = + ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, + factoryProducer = { factory } ) - var mActivity: BaseActivity<*>? = null + var mActivity: BaseActivity? = null private var shortAnimationDuration by Delegates.notNull() @@ -39,7 +40,7 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mActivity = requireActivity() as BaseActivity<*> + mActivity = requireActivity() as BaseActivity configureObserver() } @@ -75,7 +76,7 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) } fun setTitle(title: String) { - (requireActivity() as BaseActivity<*>).setTitleInActionBar(title) + (requireActivity() as BaseActivity).setTitleInActionBar(title) } fun popBackStack() = mActivity?.popBackStack() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt index 99004a6..403d5e3 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -13,8 +13,8 @@ const val SORT = "SORT" const val ORDER = "ORDER" const val DESCRIPTION = "DESCRIPTION" -const val TIME_IN = "TIME_IN" -const val TIME_OUT = "TIME_OUT" +const val DATE_IN = "TIME_IN" +const val DATE_OUT = "TIME_OUT" const val TYPE = "TYPE" class PreferenceProvider( @@ -47,8 +47,8 @@ class PreferenceProvider( ) { preference.edit() .putString(DESCRIPTION, description) - .putString(TIME_IN, timeIn) - .putString(TIME_OUT, timeOut) + .putString(DATE_IN, timeIn) + .putString(DATE_OUT, timeOut) .putString(TYPE, type) .apply() } @@ -56,8 +56,8 @@ class PreferenceProvider( fun getFilteringDetails(): Map { return mapOf( Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)), - Pair(TIME_IN, preference.getString(TIME_IN, null)), - Pair(TIME_OUT, preference.getString(TIME_OUT, null)), + Pair(DATE_IN, preference.getString(DATE_IN, null)), + Pair(DATE_OUT, preference.getString(DATE_OUT, null)), Pair(TYPE, preference.getString(TYPE, null)) ) } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt index 745fb68..400c5a4 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -14,9 +14,9 @@ import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.setDatePicker -import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.FilterViewModel -class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), +class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), AdapterView.OnItemSelectedListener, OnClickListener { private val spinnerList: Array = arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type) @@ -26,10 +26,10 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ private lateinit var dateToET: EditText private lateinit var typeSpinner: Spinner - private var description: String? = null - private var dateFrom: String? = null - private var dateTo: String? = null - private var type: String? = null + private var descriptionString: String? = null + private var dateFromString: String? = null + private var dateToString: String? = null + private var typeString: String? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -47,21 +47,29 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ val filterDetails = viewModel.getFiltrationDetails() - filterDetails.let { - LocationET.setText(it.description) - dateFromET.setText(it.dateFrom) - dateToET.setText(it.dateTo) - - it.type?.let { t -> - val spinnerPosition: Int = adapter.getPosition(t) + filterDetails.run { + description?.let { + LocationET.setText(it) + descriptionString = it + } + dateFrom?.let { + dateFromET.setText(it) + dateFromString = it + } + dateTo?.let { + dateToET.setText(it) + dateToString = it + } + type?.let { + typeString = it + val spinnerPosition: Int = adapter.getPosition(it) typeSpinner.setSelection(spinnerPosition) } - } - LocationET.doAfterTextChanged { description = it.toString() } - dateFromET.setDatePicker { dateFrom = it } - dateToET.setDatePicker { dateTo = it } + LocationET.doAfterTextChanged { descriptionString = it.toString() } + dateFromET.setDatePicker { dateFromString = it } + dateToET.setDatePicker { dateToString = it } typeSpinner.onItemSelectedListener = this submit.setOnClickListener(this) @@ -73,7 +81,7 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ position: Int, id: Long ) { - type = when (position) { + typeString = when (position) { 1 -> ShiftType.HOURLY.type 2 -> ShiftType.PIECE.type else -> return @@ -83,7 +91,7 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ override fun onNothingSelected(parentView: AdapterView<*>?) {} private fun submitFiltrationDetails() { - viewModel.setFiltrationDetails(description, dateFrom, dateTo, type) + viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString) } override fun onClick(p0: View?) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index eb74f10..c0a09b9 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -26,8 +26,9 @@ import com.appttude.h_mal.farmr.utils.setTimePicker import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.utils.validateField import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel -class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), +class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), RadioGroup.OnCheckedChangeListener, BackPressedListener { private lateinit var mHourlyRadioButton: RadioButton @@ -262,7 +263,6 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), StringBuilder().append(mDuration).append(" hours").toString() mDuration!! * mPayRate } - ShiftType.PIECE -> { (mUnits ?: 0f) * mPayRate } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index f083778..e0f1cbd 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -70,7 +70,6 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr override fun onStart() { super.onStart() - viewModel.refreshLiveData() } @@ -112,7 +111,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } R.id.clear_filter -> { - viewModel.setFiltrationDetails(null, null, null, null) + viewModel.clearFilters() return true } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt index bda6dfd..e536e10 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt @@ -11,13 +11,14 @@ import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.utils.CURRENCY -import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString +import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.utils.show -import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.InfoViewModel -class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { +class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { private lateinit var typeTV: TextView private lateinit var descriptionTV: TextView private lateinit var dateTV: TextView @@ -52,60 +53,50 @@ class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher hourlyDetailHolder = view.findViewById(R.id.details_hourly_details) unitsHolder = view.findViewById(R.id.details_units_holder) - val id = arguments!!.getLong(ID) - editButton.setOnClickListener { navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!) } - setupView(id) + viewModel.retrieveData(arguments) } - private fun setupView(id: Long) { - viewModel.getCurrentShift(id)?.run { - typeTV.text = type - descriptionTV.text = description - dateTV.text = date - payRateTV.text = rateOfPay.toString() - totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is ShiftObject) data.setupView() + } - when (ShiftType.getEnumByType(type)) { - ShiftType.HOURLY -> { - hourlyDetailHolder.show() - unitsHolder.hide() - times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() - breakTV.text = StringBuilder(breakMins).append("mins").toString() - durationTV.text = buildDurationSummary(this) - val paymentSummary = - StringBuilder().append(duration).append(" Hours @ ").append(CURRENCY) - .append(rateOfPay).append(" per Hour").append("\n") - .append("Equals: ").append(CURRENCY).append(totalPay) - totalPayTV.text = paymentSummary - } + private fun ShiftObject.setupView() { + typeTV.text = type + descriptionTV.text = description + dateTV.text = date + payRateTV.text = rateOfPay.toString() + totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() - ShiftType.PIECE -> { - hourlyDetailHolder.hide() - unitsHolder.show() - unitsTV.text = units.toString() + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + hourlyDetailHolder.show() + unitsHolder.hide() + times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() + breakTV.text = StringBuilder().append(breakMins).append(" mins").toString() + durationTV.text = viewModel.buildDurationSummary(this) + val paymentSummary = + StringBuilder().append(duration).append(" Hours @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary + } - val paymentSummary = - StringBuilder().append(units).append(" Units @ ").append(CURRENCY) - .append(rateOfPay).append(" per Unit").append("\n") - .append("Equals: ").append(CURRENCY).append(totalPay) - totalPayTV.text = paymentSummary - } + ShiftType.PIECE -> { + hourlyDetailHolder.hide() + unitsHolder.show() + unitsTV.text = units.toString() + + val paymentSummary = + StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary } } } - - private fun buildDurationSummary(shiftObject: ShiftObject): String { - val time = shiftObject.getHoursMinutesPairFromDuration() - - val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) - .append(" Minutes ") - if (shiftObject.breakMins > 0) { - stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") - } - return stringBuilder.toString() - } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt index 187d4cd..245b54f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -19,7 +19,7 @@ import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.MainViewModel import kotlin.system.exitProcess -class MainActivity : BaseActivity() { +class MainActivity : BaseActivity() { private lateinit var toolbar: Toolbar override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt index 00f9119..5b007e1 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -1,8 +1,10 @@ package com.appttude.h_mal.farmr.utils import java.io.IOException +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Currency import java.util.Date import java.util.Locale @@ -16,6 +18,14 @@ fun Float.formatToTwoDp(): Float { return formattedString.toFloat() } +fun Float.formatAsCurrencyString(): String? { + val format: NumberFormat = NumberFormat.getCurrencyInstance() + format.maximumFractionDigits = 2 + format.currency = Currency.getInstance("GBP") + + return format.format(this) +} + fun Float.formatToTwoDpString(): String { return formatToTwoDp().toString() } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt index 16883c7..73cc067 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt @@ -14,6 +14,9 @@ class ApplicationViewModelFactory( with(modelClass) { return when { isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository) + isAssignableFrom(SubmissionViewModel::class.java) -> SubmissionViewModel(repository) + isAssignableFrom(InfoViewModel::class.java) -> InfoViewModel(repository) + isAssignableFrom(FilterViewModel::class.java) -> FilterViewModel(repository) else -> throw IllegalArgumentException("Unknown ViewModel class") } as T } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt new file mode 100644 index 0000000..87bb2bd --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Success + + +class FilterViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun applyFilters( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + super.setFiltrationDetails(description, dateFrom, dateTo, type) + onSuccess(Success("Filter(s) have been applied")) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt new file mode 100644 index 0000000..dea7585 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt @@ -0,0 +1,40 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.os.Bundle +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.ID + + +class InfoViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun retrieveData(bundle: Bundle?) { + val id = bundle?.getLong(ID) + if (id == null) { + onError("Failed to retrieve shift") + return + } + + val shift = getCurrentShift(id) + if (shift == null) { + onError("Failed to retrieve shift") + return + } + + onSuccess(shift) + } + + fun buildDurationSummary(shiftObject: ShiftObject): String { + val time = shiftObject.getHoursMinutesPairFromDuration() + + val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) + .append(" Minutes ") + if (shiftObject.breakMins > 0) { + stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") + } + return stringBuilder.toString() + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index bec425b..7a99874 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -1,13 +1,10 @@ package com.appttude.h_mal.farmr.viewmodel import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.os.Build -import android.os.Environment import androidx.annotation.RequiresPermission import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import com.appttude.h_mal.farmr.base.BaseViewModel import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK @@ -21,24 +18,14 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID -import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION -import com.appttude.h_mal.farmr.data.prefs.TIME_IN -import com.appttude.h_mal.farmr.data.prefs.TIME_OUT -import com.appttude.h_mal.farmr.data.prefs.TYPE -import com.appttude.h_mal.farmr.model.FilterStore import com.appttude.h_mal.farmr.model.Order -import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.CURRENCY -import com.appttude.h_mal.farmr.utils.calculateDuration import com.appttude.h_mal.farmr.utils.convertDateString -import com.appttude.h_mal.farmr.utils.dateStringIsValid -import com.appttude.h_mal.farmr.utils.formatToTwoDp -import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.sortedByOrder -import com.appttude.h_mal.farmr.utils.timeStringIsValid import jxl.Workbook import jxl.WorkbookSettings import jxl.write.Label @@ -46,25 +33,24 @@ import jxl.write.WritableWorkbook import jxl.write.WriteException import java.io.File import java.io.IOException -import java.util.Calendar import java.util.Locale class MainViewModel( private val repository: Repository -) : BaseViewModel() { +) : ShiftViewModel(repository) { private val _shiftLiveData = MutableLiveData>() - val shiftLiveData: LiveData> = _shiftLiveData + private val shiftLiveData: LiveData> = _shiftLiveData private var mSort: Sortable = Sortable.ID private var mOrder: Order = Order.ASCENDING - private var mFilterStore: FilterStore? = null - private val observer = Observer> { - val result = it.applyFilters().sortList(mSort, mOrder) - onSuccess(result) + it?.let { + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) + } } init { @@ -148,8 +134,9 @@ class MainViewModel( var countOfTypeP = 0 var totalUnits = 0f var totalPay = 0f - val lines = _shiftLiveData.value?.size ?: 0 - _shiftLiveData.value?.forEach { + var lines = 0 + _shiftLiveData.value?.applyFilters()?.forEach { + lines += 1 totalDuration += it.duration when (ShiftType.getEnumByType(it.type)) { ShiftType.HOURLY -> countOfTypeH += 1 @@ -169,161 +156,6 @@ class MainViewModel( ) } - fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) - - fun insertHourlyShift( - description: String, - date: String, - rateOfPay: Float, - timeIn: String?, - timeOut: String?, - breakMins: Int?, - ) { - // Validate inputs from the edit texts - (description.length > 3).validateField { - onError("Description length should be longer") - return - } - date.dateStringIsValid().validateField { - onError("Date format is invalid") - return - } - (rateOfPay >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - timeIn?.timeStringIsValid()?.validateField { - onError("Time in format is in correct") - return - } - timeOut?.timeStringIsValid()?.validateField { - onError("Time out format is in correct") - return - } - breakMins?.let { it > 0 }?.validateField { - onError("Break in minutes is invalid") - return - } - - doTry { - val result = insertShiftIntoDatabase( - ShiftType.HOURLY, - description, - date, - rateOfPay.formatToTwoDp(), - timeIn, - timeOut, - breakMins, - null - ) - - if (result) onSuccess(Success("Shift successfully added")) - } - - } - - fun insertPieceRateShift( - description: String, - date: String, - units: Float, - rateOfPay: Float - ) { - // Validate inputs from the edit texts - (description.length > 3).validateField { - onError("Description length should be longer") - return - } - date.dateStringIsValid().validateField { - onError("Date format is invalid") - return - } - (rateOfPay >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - (units.toInt() >= 0).validateField { - onError("Units cannot be below zero") - return - } - - doTry { - val result = insertShiftIntoDatabase( - type = ShiftType.PIECE, - description = description, - date = date, - rateOfPay = rateOfPay.formatToTwoDp(), - null, - null, - null, - units = units - ) - if (result) onSuccess(Success("New shift successfully added")) - } - } - - fun updateShift( - id: Long, - type: String? = null, - description: String? = null, - date: String? = null, - rateOfPay: Float? = null, - timeIn: String? = null, - timeOut: String? = null, - breakMins: Int? = null, - units: Float? = null, - ) { - description?.let { - (it.length > 3).validateField { - onError("Description length should be longer") - return - } - } - date?.dateStringIsValid()?.validateField { - onError("Date format is invalid") - return - } - rateOfPay?.let { - (it >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - } - units?.let { - (it.toInt() >= 0).validateField { - onError("Units cannot be below zero") - return - } - } - timeIn?.timeStringIsValid()?.validateField { - onError("Time in format is in correct") - return - } - timeOut?.timeStringIsValid()?.validateField { - onError("Time out format is in correct") - return - } - breakMins?.let { it >= 0 }?.validateField { - onError("Break in minutes is invalid") - return - } - - doTry { - val result = updateShiftInDatabase( - id, - type = type?.let { ShiftType.getEnumByType(it) }, - description = description, - date = date, - rateOfPay = rateOfPay, - timeIn = timeIn, - timeOut = timeOut, - breakMins = breakMins, - units = units - ) - - if (result) onSuccess(Success("Shift successfully updated")) - } - } - fun deleteShift(id: Long) { if (!repository.deleteSingleShiftFromDatabase(id)) { onError("Failed to delete shift") @@ -340,134 +172,6 @@ class MainViewModel( } } - private fun updateShiftInDatabase( - id: Long, - type: ShiftType? = null, - description: String? = null, - date: String? = null, - rateOfPay: Float? = null, - timeIn: String? = null, - timeOut: String? = null, - breakMins: Int? = null, - units: Float? = null, - ): Boolean { - val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() - ?: throw IOException("Cannot update shift as it does not exist") - - val shift = when (type) { - ShiftType.HOURLY -> { - // Shift type has changed so mandatory fields for hourly shift are now required as well - val insertTimeIn = - (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") - val insertTimeOut = - (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - ShiftType.PIECE -> { - // Shift type has changed so mandatory fields for piece rate shift are now required as well - val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - else -> { - if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { - // Updates to description or date field - currentShift.copy( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - ) - } else { - // Updating shifts where shift type has remained the same - when (currentShift.type) { - ShiftType.HOURLY -> { - val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( - "No time in inserted" - ) - val insertTimeOut = (timeOut ?: currentShift.timeOut) - ?: throw IOException("No time out inserted") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - ShiftType.PIECE -> { - val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - } - } - } - } - - return repository.updateShiftIntoDatabase(id, shift) - } - - private fun insertShiftIntoDatabase( - type: ShiftType, - description: String, - date: String, - rateOfPay: Float, - timeIn: String?, - timeOut: String?, - breakMins: Int?, - units: Float?, - ): Boolean { - val shift = when (type) { - ShiftType.HOURLY -> { - if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") - val calendar by lazy { Calendar.getInstance() } - val insertTimeIn = timeIn ?: calendar.getTimeString() - val insertTimeOut = timeOut ?: calendar.getTimeString() - - Shift( - description = description, - date = date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins, - rateOfPay = rateOfPay - ) - } - - ShiftType.PIECE -> { - Shift( - description = description, - date = date, - units = units!!, - rateOfPay = rateOfPay, - ) - } - } - - return repository.insertShiftIntoDatabase(shift) - } - - private fun buildInfoString( totalDuration: Float, countOfHourly: Int, @@ -488,63 +192,21 @@ class MainViewModel( stringBuilder.append("Total Units: ").append(totalUnits).append("\n") } if (totalPay != 0f) { - stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n") + stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString()) } return stringBuilder.toString() } fun refreshLiveData() { - _shiftLiveData.postValue(repository.readShiftsFromDatabase()) + repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) } } - private inline fun Boolean.validateField(failureCallback: () -> Unit) { - if (!this) failureCallback.invoke() - } - - /** - * Lambda function that will invoke onError(...) on failure - * but update live data when successful - */ - private inline fun doTry(operation: () -> Unit) { - try { - operation.invoke() - refreshLiveData() - } catch (e: Exception) { - onError(e) - } - } - - fun setFiltrationDetails( - description: String?, - dateFrom: String?, - dateTo: String?, - type: String? - ) { - repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) - onSuccess(Success("Filter(s) successfully applied")) + fun clearFilters() { + super.setFiltrationDetails(null, null, null, null) + onSuccess(Success("Filters have been cleared")) refreshLiveData() } - fun getFiltrationDetails(): FilterStore { - val prefs = repository.retrieveFilteringDetailsInPrefs() - mFilterStore = FilterStore( - prefs[DESCRIPTION], - prefs[TIME_IN], - prefs[TIME_OUT], - prefs[TYPE] - ) - return mFilterStore!! - } - - fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { - try { - return calculateDuration(mTimeIn, mTimeOut, mBreaks) - } catch (e: IOException) { - onError(e) - } - return null - } - @RequiresPermission(WRITE_EXTERNAL_STORAGE) fun createExcelSheet(file: File): File? { val wbSettings = WorkbookSettings().apply { @@ -574,7 +236,8 @@ class MainViewModel( return null } val sortAndOrder = getSortAndOrder() - val data = shiftLiveData.value!!.applyFilters().sortList(sortAndOrder.first, sortAndOrder.second) + val data = shiftLiveData.value!!.applyFilters() + .sortList(sortAndOrder.first, sortAndOrder.second) var currentRow = 0 val cells = data.mapIndexed { index, shift -> currentRow += 1 diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt new file mode 100644 index 0000000..33c7ea5 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt @@ -0,0 +1,52 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.base.BaseViewModel +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.FilterStore + + +open class ShiftViewModel( + private val repository: Repository +) : BaseViewModel() { + + /* + * Add Item & Further info + */ + fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) + + /** + * Lambda function that will invoke onError(...) on failure + * but update live data when successful + */ + private inline fun doTry(operation: () -> Unit) { + try { + operation.invoke() + } catch (e: Exception) { + onError(e) + } + } + + open fun setFiltrationDetails( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + } + + open fun getFiltrationDetails(): FilterStore { + val prefs = repository.retrieveFilteringDetailsInPrefs() + return FilterStore( + prefs[DESCRIPTION], + prefs[DATE_IN], + prefs[DATE_OUT], + prefs[TYPE] + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt new file mode 100644 index 0000000..322ca40 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt @@ -0,0 +1,308 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.dateStringIsValid +import com.appttude.h_mal.farmr.utils.formatToTwoDp +import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.timeStringIsValid +import java.io.IOException +import java.util.Calendar + + +class SubmissionViewModel( + private val repository: Repository +) : ShiftViewModel(repository) { + + fun insertHourlyShift( + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = insertShiftIntoDatabase( + ShiftType.HOURLY, + description, + date, + rateOfPay.formatToTwoDp(), + timeIn, + timeOut, + breakMins, + null + ) + + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun insertPieceRateShift( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + (units.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + + val result = insertShiftIntoDatabase( + type = ShiftType.PIECE, + description = description, + date = date, + rateOfPay = rateOfPay.formatToTwoDp(), + null, + null, + null, + units = units + ) + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun updateShift( + id: Long, + type: String? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ) { + description?.let { + (it.length > 3).validateField { + onError("Description length should be longer") + return + } + } + date?.dateStringIsValid()?.validateField { + onError("Date format is invalid") + return + } + rateOfPay?.let { + (it >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + } + units?.let { + (it.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = updateShiftInDatabase( + id, + type = type?.let { ShiftType.getEnumByType(it) }, + description = description, + date = date, + rateOfPay = rateOfPay, + timeIn = timeIn, + timeOut = timeOut, + breakMins = breakMins, + units = units + ) + + if (result) onSuccess(Success("Shift successfully updated")) + else onError("Cannot update shift") + } + + private fun updateShiftInDatabase( + id: Long, + type: ShiftType? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ): Boolean { + val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() + ?: throw IOException("Cannot update shift as it does not exist") + + val shift = when (type) { + ShiftType.HOURLY -> { + // Shift type has changed so mandatory fields for hourly shift are now required as well + val insertTimeIn = + (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") + val insertTimeOut = + (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + // Shift type has changed so mandatory fields for piece rate shift are now required as well + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + else -> { + if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { + // Updates to description or date field + currentShift.copy( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + ) + } else { + // Updating shifts where shift type has remained the same + when (currentShift.type) { + ShiftType.HOURLY -> { + val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( + "No time in inserted" + ) + val insertTimeOut = (timeOut ?: currentShift.timeOut) + ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + } + } + } + } + + return repository.updateShiftIntoDatabase(id, shift) + } + + private fun insertShiftIntoDatabase( + type: ShiftType, + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + units: Float?, + ): Boolean { + val shift = when (type) { + ShiftType.HOURLY -> { + if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + Shift( + description = description, + date = date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins, + rateOfPay = rateOfPay + ) + } + + ShiftType.PIECE -> { + Shift( + description = description, + date = date, + units = units!!, + rateOfPay = rateOfPay, + ) + } + } + + return repository.insertShiftIntoDatabase(shift) + } + + private inline fun Boolean.validateField(failureCallback: () -> Unit) { + if (!this) failureCallback.invoke() + } + + fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { + try { + return calculateDuration(mTimeIn, mTimeOut, mBreaks) + } catch (e: IOException) { + onError(e) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt new file mode 100644 index 0000000..127dab4 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt @@ -0,0 +1,239 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import java.util.concurrent.TimeoutException +import kotlin.test.assertEquals + +class MainViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + private lateinit var repository: Repository + private lateinit var viewModel: MainViewModel + + @Before + fun setUp() { + repository = mockk() + every { repository.readShiftsFromDatabase() }.returns(null) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel = MainViewModel(repository) + } + + @Test + fun initViewModel_liveDataIsEmpty() { + // Assert + assertThrows(TimeoutException::class.java) { viewModel.uiState.getOrAwaitValue() } + } + + @Test + fun getShiftsFromRepository_liveDataIsShown() { + // Arrange + val listOfShifts = anyList() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + + // Assert + assertEquals(retrieveCurrentData(), listOfShifts) + } + + @Test + fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() { + // Arrange + val listOfShifts = getShifts() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, listOfShifts) + assertEquals( + description, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + @Test + fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() { + // Arrange + val listOfShifts = getShifts() + val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type } + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type)) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + every { repository.setFilteringDetailsInPrefs(null, null, null, null) }.returns(Unit) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel.clearFilters() + val descriptionAfterClearedFilter = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, filteredShifts) + assertEquals( + description, "4 Shifts\n" + + "Total Hours: 4.0\n" + + "Total Pay: £30.00" + ) + assertEquals( + descriptionAfterClearedFilter, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + private fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + private fun getFilter( + description: String? = null, + type: String? = null, + dateIn: String? = null, + dateOut: String? = null + ): Map = + mapOf( + Pair(DESCRIPTION, description), + Pair(DATE_IN, dateIn), + Pair(DATE_OUT, dateOut), + Pair(TYPE, type) + ) + + private fun getShifts() = listOf( + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + anyInt(), + anyFloat(), + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + anyInt(), + anyFloat(), + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + anyFloat(), + 10f, + 5f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + anyFloat(), + 10f, + 5f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt new file mode 100644 index 0000000..1106249 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt @@ -0,0 +1,171 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import org.junit.Before +import org.junit.Rule +import org.mockito.ArgumentMatchers + +open class ShiftViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + @RelaxedMockK + lateinit var repository: Repository + + @InjectMockKs + lateinit var viewModel: V + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + + fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + fun retrieveCurrentError() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasError<*>).error + + fun getHourlyShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ) + + fun getPieceRateShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ) + + fun getShifts() = listOf( + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt new file mode 100644 index 0000000..2f6244b --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt @@ -0,0 +1,85 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.model.Success +import io.mockk.every +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertIs + +class SubmissionViewModelTest : ShiftViewModelTest() { + + @Test + fun insertHourlyShifts_validParameters_successfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(true) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertPieceShifts_validParameters_successfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(true) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertHourlyShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(false) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + + @Test + fun insertPieceShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(false) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + +} \ No newline at end of file From ce5be162321c30ac0511f7b4db93447239a49ed5 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 14:02:42 +0100 Subject: [PATCH 09/15] - Test suite expanded - config.yml updated - --- .circleci/config.yml | 140 ++++++++++++++--- .gitignore | 2 +- .idea/androidTestResultsUserPreferences.xml | 22 --- .idea/assetWizardSettings.xml | 127 ---------------- .idea/caches/build_file_checksums.ser | Bin 537 -> 0 bytes .idea/caches/gradle_models.ser | Bin 128383 -> 0 bytes .idea/compiler.xml | 6 - .idea/gradle.xml | 21 --- .idea/jarRepositories.xml | 30 ---- .idea/misc.xml | 47 ------ .idea/modules.xml | 12 -- .idea/vcs.xml | 6 - app/build.gradle | 3 +- .../h_mal/farmr/application/TestAppClass.kt | 38 +++++ .../h_mal/farmr/application/TestRunner.kt | 21 +++ .../h_mal/farmr/data/ShiftProviderTest.kt | 6 + .../farmr/data/ui/robots/HomeScreenRobot.kt | 26 ---- .../h_mal/farmr/data/ui/tests/ShiftTests.kt | 77 ---------- .../h_mal/farmr/data/ui/utils/Constants.kt | 1 - .../h_mal/farmr/{data => }/ui/BaseTest.kt | 27 +++- .../farmr/{data => }/ui/BaseTestRobot.kt | 96 +++++++++--- .../ui/robots/AddItemScreenRobot.kt | 4 +- .../{data => }/ui/robots/FilterScreenRobot.kt | 4 +- .../h_mal/farmr/ui/robots/HomeScreenRobot.kt | 29 ++++ .../ui/robots/ViewItemScreenRobot.kt | 4 +- .../h_mal/farmr/ui/tests/ShiftTests.kt | 142 ++++++++++++++++++ .../h_mal/farmr/ui/utils/Constants.kt | 1 + .../h_mal/farmr/ui/utils/DataHelper.kt | 103 +++++++++++++ .../{data => }/ui/utils/EspressoHelper.kt | 2 +- .../farmr/{data => }/ui/utils/TestUtils.kt | 2 +- .../h_mal/farmr/base/BaseApplication.kt | 31 ++++ .../farmr/data/legacydb/ShiftsContract.kt | 3 +- .../farmr/data/prefs/PreferencesProvider.kt | 4 + .../h_mal/farmr/di/ShiftApplication.kt | 20 +-- .../appttude/h_mal/farmr/model/Sortable.kt | 4 + .../h_mal/farmr/ui/FragmentAddItem.kt | 7 +- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 2 +- .../appttude/h_mal/farmr/ui/SplashScreen.kt | 8 +- 38 files changed, 631 insertions(+), 447 deletions(-) delete mode 100644 .idea/androidTestResultsUserPreferences.xml delete mode 100644 .idea/assetWizardSettings.xml delete mode 100644 .idea/caches/build_file_checksums.ser delete mode 100644 .idea/caches/gradle_models.ser delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/BaseTest.kt (77%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/BaseTestRobot.kt (65%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/AddItemScreenRobot.kt (92%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/FilterScreenRobot.kt (90%) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/ViewItemScreenRobot.kt (92%) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/utils/EspressoHelper.kt (98%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/utils/TestUtils.kt (94%) create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index 4175da6..342196f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,26 +1,130 @@ # Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/configuration-reference +# See: https://circleci.com/docs/2.0/configuration-reference +# For a detailed guide to building and testing on Android, read the docs: +# https://circleci.com/docs/2.0/language-android/ for more details. version: 2.1 -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/configuration-reference/#jobs -jobs: - say-hello: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/configuration-reference/#executor-job - docker: - - image: cimg/base:stable - # Add steps to the job - # See: https://circleci.com/docs/configuration-reference/#steps +# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. +# See: https://circleci.com/docs/2.0/orb-intro/ +orbs: + android: circleci/android@2.3.0 + +commands: + setup_repo: + description: checkout repo and android dependencies steps: - checkout - run: - name: "Say hello" - command: "echo Hello, World!" - -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/configuration-reference/#workflows + name: Give gradle permissions + command: | + sudo chmod +x ./gradlew + - android/restore-gradle-cache + run_tests: + description: run tests for flavour specified + steps: + # The next step will run the unit tests + - run: + name: Run local unit tests + command: | + ./gradlew testDebugUnitTest + - android/save-gradle-cache + - store_artifacts: + path: app/build/reports + destination: reports + - store_test_results: + path: app/build/test-results + run_ui_tests: + description: run instrumentation and espresso tests + steps: + - android/start-emulator-and-run-tests: + post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest + test-command: ./gradlew connectedDebugAndroidTest --continue + system-image: system-images;android-26;google_apis;x86 + # store screenshots for failed ui tests + - when: + condition: on_fail + steps: + - store_artifacts: + path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected + destination: connected_android_test + # store test reports + - store_artifacts: + path: app/build/reports/androidTests/connected + destination: reports + - store_test_results: + path: app/build/outputs/androidTest-results/connected + deploy_to_play_store: + description: deploy to playstore + steps: + # The next step will run the unit tests + - android/decode-keystore: + keystore-location: "./app/keystore.jks" + - run: + name: Setup playstore key + command: | + echo "$GOOGLE_PLAY_KEY" > "google-play-key.json" + - run: + name: Run fastlane command to deploy to playstore + command: | + pwd + bundle exec fastlane deploy + - store_test_results: + path: fastlane/report.xml +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + # Below is the definition of your job to build and test your app, you can rename and customize it as you want. + build-and-test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_tests + run_instrumentation_test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_ui_tests + deploy-to-playstore: + docker: + - image: cimg/android:2023.07-browsers + auth: + username: ${DOCKER_USERNAME} + password: ${DOCKER_PASSWORD} + steps: + - setup_repo + - deploy_to_play_store +# Invoke jobs via workflows +# See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: - say-hello-workflow: + version: 2 + build-release: jobs: - - say-hello + - build-and-test: + context: appttude + - run_instrumentation_test: + context: appttude + filters: + branches: + only: + - master + - release + - deploy-to-playstore: + context: appttude + filters: + branches: + only: + - release + requires: + - build-and-test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87e70e7..ae7f176 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,7 @@ gen-external-apklibs .idea/uiDesigner.xml .idea/assetWizardSettings.xml .idea/gradle.xml -.idea/jarRepositorie +.idea/jarRepositories.xml # Gem/fastlane Gemfile.lock diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml deleted file mode 100644 index 263c04c..0000000 --- a/.idea/androidTestResultsUserPreferences.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 0068d43..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index 3f6cd0f69fb12a71725208e92a594b5506cee1cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 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≈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 - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 1431050..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index eb2873e..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 5ac489c..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8c565d6..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 86f0e87..3bba2f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { targetSdkVersion 31 versionCode 1 versionName "1.0" - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true } buildTypes { @@ -19,6 +19,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + useLibrary 'android.test.mock' } dependencies { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt new file mode 100644 index 0000000..616c325 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt @@ -0,0 +1,38 @@ +package com.appttude.h_mal.farmr.application + +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.base.BaseApplication +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.model.Shift + +class TestAppClass : BaseApplication() { + private val idlingResources = CountingIdlingResource("Data_loader") + + lateinit var database: LegacyDatabase + lateinit var preferenceProvider: PreferenceProvider + + override fun onCreate() { + super.onCreate() + IdlingRegistry.getInstance().register(idlingResources) + } + + override fun createDatabase(): LegacyDatabase { + database = + LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver) + return database + } + + override fun createPrefs(): PreferenceProvider { + preferenceProvider = PreferenceProvider(this) + return preferenceProvider + } + + fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift) + fun addShiftsToDatabase(shifts: List) = shifts.forEach { addToDatabase(it) } + fun clearDatabase() = database.deleteAllShiftsInDatabase() + fun cleanPrefs() = preferenceProvider.clearPrefs() + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt new file mode 100644 index 0000000..ee712d4 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.application + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +class TestRunner : AndroidJUnitRunner() { + @Throws( + InstantiationException::class, + IllegalAccessException::class, + ClassNotFoundException::class + ) + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, TestAppClass::class.java.name, context) + } + +} diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt index 67a4c51..7a11af7 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt @@ -19,6 +19,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull +import org.junit.After import org.junit.Rule import org.junit.Test @@ -31,6 +32,11 @@ class ShiftProviderTest { private val contentResolver: ContentResolver get() = providerRule.resolver + @After + fun tearDown() { + contentResolver.delete(CONTENT_URI, null, null) + } + @Test fun insertEntry_queryEntry_assertEntry() { // Arrange diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt deleted file mode 100644 index 4f5c5e6..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.robots - -import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot - -fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } -class HomeScreenRobot : BaseTestRobot() { - - fun clickOnItem(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position) - fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) - fun clickOnEdit(position: Int) = clickViewInRecycler(R.id.list_item_view, R.id.imageView) - fun clickFab() = clickButton(R.id.fab1) - fun clickOnInfo() = clickButton(R.id.action_favorite) -// fun clearFilter() = -// fun applySort() = - - -// fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location) -// fun refresh() = pullToRefresh(R.id.swipe_refresh) -// fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4) -// fun verifyUnableToRetrieve() { -// matchText(R.id.header_text, R.string.retrieve_warning) -// matchText(R.id.body_text, R.string.empty_retrieve_warning) -// } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt deleted file mode 100644 index 53997bd..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.tests - -import com.appttude.h_mal.farmr.data.ui.BaseTest -import com.appttude.h_mal.farmr.data.ui.robots.addScreen -import com.appttude.h_mal.farmr.data.ui.robots.homeScreen -import com.appttude.h_mal.farmr.data.ui.robots.viewScreen -import com.appttude.h_mal.farmr.model.ShiftType -import com.appttude.h_mal.farmr.ui.MainActivity -import org.junit.Test - -class ShiftTests: BaseTest(MainActivity::class.java) { - - // Add a shift successfully - @Test - fun test1() { - homeScreen { - clickFab() - } - addScreen { - setDescription("This is a description") - setDate(2023, 2, 11) - clickShiftType(ShiftType.HOURLY) - setTimeIn(12,0) - setTimeOut(14, 30) - setBreakTime(30) - setRateOfPay(10f) - assertDuration("2.0 hours") - assertTotalPay("£20.00") - submit() - } - homeScreen { - sc("This is a description") - } - } - - // Edit a shift successfully - @Test - fun test2() { - homeScreen { - clickOnItemWithText("Edit this shift") - } - addScreen { - setRateOfPay(20f) - assertDuration("2.0 hours") - assertTotalPay("£40.00") - submit() - } - homeScreen { - clickOnItemWithText("Edit this shift") - } - viewScreen { - matchDescription("Edit this shift") - matchDuration("2 Hours 0 minutes") - matchTotalPay("2.0 hours @ £20.00 per Hour\nEquals:£40.00") - } - } - - // filter the list with date from - @Test - fun test3() {} - - // filter the list with date to - @Test - fun test4() {} - - // Add a shift as piece rate - @Test - fun test5() {} - - // Validate the details screen - @Test - fun test6() {} - - // filter, sort, order and then reset - @Test - fun test7() {} -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt deleted file mode 100644 index e18ec2c..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt +++ /dev/null @@ -1 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt similarity index 77% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt index 87e8ed5..0536254 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui +package com.appttude.h_mal.farmr.ui import android.Manifest import android.app.Activity @@ -6,7 +6,9 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View +import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -15,13 +17,16 @@ import androidx.test.espresso.matcher.RootMatchers.withDecorView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import com.appttude.h_mal.farmr.application.TestAppClass import com.appttude.h_mal.farmr.di.ShiftApplication +import com.appttude.h_mal.farmr.ui.utils.getShifts import kotlinx.coroutines.runBlocking import org.hamcrest.Matcher import org.hamcrest.Matchers import org.junit.After import org.junit.Before import org.junit.Rule +import org.kodein.di.android.kodein @Suppress("EmptyMethod") open class BaseTest( @@ -30,7 +35,7 @@ open class BaseTest( ) { lateinit var scenario: ActivityScenario - private lateinit var testApp: ShiftApplication + private lateinit var testApp: TestAppClass private lateinit var testActivity: Activity private lateinit var decorView: View @@ -38,7 +43,7 @@ open class BaseTest( var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) @Before - fun setUp() { + open fun setUp() { val startIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity) if (intentBundle != null) { @@ -46,13 +51,17 @@ open class BaseTest( } testApp = - InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ShiftApplication + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + kodein(testApp) runBlocking { beforeLaunch() } scenario = ActivityScenario.launch(startIntent) scenario.onActivity { + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + onLaunch() decorView = it.window.decorView testActivity = it } @@ -67,6 +76,7 @@ open class BaseTest( } open fun beforeLaunch() {} + open fun onLaunch() {} open fun afterLaunch() {} open fun testFinished() {} @@ -88,4 +98,13 @@ open class BaseTest( waitFor(3500) } } + + fun navigateBack() = Espresso.pressBack() + + fun addRandomShifts() { + testApp.addShiftsToDatabase(getShifts()) + } + + fun clearDataBase() = testApp.clearDatabase() + fun clearPrefs() = testApp.cleanPrefs() } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt similarity index 65% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt index d864acb..a85098f 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt @@ -1,12 +1,18 @@ -package com.appttude.h_mal.farmr.data.ui +package com.appttude.h_mal.farmr.ui import android.content.res.Resources +import android.view.View import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.StringRes +import androidx.appcompat.widget.AppCompatButton +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click @@ -14,11 +20,15 @@ import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.* -import com.appttude.h_mal.farmr.data.ui.utils.EspressoHelper.waitForView +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.Matcher +import org.hamcrest.Matchers @SuppressWarnings("unused") open class BaseTestRobot { @@ -54,6 +64,17 @@ open class BaseTestRobot { .atPosition(position).perform(click()) } + fun clickOnMenuItem(menuId: Int) { + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context) + onView(withText(menuId)).perform(click()) + } + + fun clickDialogButton(text: String) { + onView(withText(text)).inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()); + } + fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { return matchView(recyclerId) .perform( @@ -88,14 +109,6 @@ open class BaseTestRobot { ) } - fun clickViewInRecycler(recyclerId: Int, text: String) { - matchView(recyclerId) - .perform( - // scrollTo will fail the test if no item matches. - RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click()) - ) - } - fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) { matchView(recyclerId) .perform( @@ -107,23 +120,52 @@ open class BaseTestRobot { ) } - fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) { + fun clickRecyclerAtPosition(recyclerId: Int, position: Int) { matchView(recyclerId) .perform( // scrollTo will fail the test if no item matches. RecyclerViewActions.scrollToPosition(position), - RecyclerViewActions.actionOnItemAtPosition(position, click()) + RecyclerViewActions.actionOnItemAtPosition(position, click()), + ) + } + + fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, object : ViewAction { + override fun getDescription(): String { + return "click on subview in RecyclerView at position: $position" + } + + override fun getConstraints(): Matcher { + return Matchers.allOf( + isAssignableFrom( + RecyclerView::class.java + ), isDisplayed() + ) + } + + override fun perform(uiController: UiController?, view: View?) { + view?.findViewById(subViewId)?.performClick() + } + + }), ) } fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) { - scrollToRecyclerItem(recyclerId, text) - ?.perform( - // scrollTo will fail the test if no item matches. - RecyclerViewActions.actionOnItem( - withChild(withText(text)), click() - ) + matchView(recyclerId).perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ), + RecyclerViewActions.actionOnItem( + hasDescendant(withText(text)), + click() ) + ) } fun swipeDown(resId: Int): ViewInteraction = @@ -144,9 +186,17 @@ open class BaseTestRobot { day ) ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) } - fun selectTextInSpinner(id: Int, text:String) { + fun selectTextInSpinner(id: Int, text: String) { clickButton(id) onView(withSpinnerText(text)).perform(click()) } @@ -157,5 +207,13 @@ open class BaseTestRobot { hours, minutes ) ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt similarity index 92% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt index f2149b2..c28cfb1 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt @@ -1,8 +1,8 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() } class AddItemScreenRobot : BaseTestRobot() { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt similarity index 90% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt index 22e36ed..5907ac6 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt @@ -1,7 +1,7 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt new file mode 100644 index 0000000..4584b4a --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt @@ -0,0 +1,29 @@ +package com.appttude.h_mal.farmr.ui.robots + +import androidx.test.espresso.matcher.RootMatchers.isDialog +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } +class HomeScreenRobot : BaseTestRobot() { + + fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) + fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition(R.id.list_item_view, position) + fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position, R.id.imageView) + fun clickFab() = clickButton(R.id.fab1) + fun clickOnInfoIcon() = clickButton(R.id.action_favorite) + fun clickFilterInMenu() = clickOnMenuItem(R.string.filter) + fun clickClearFilterInMenu() = clickOnMenuItem(R.string.clear) + fun clickSortInMenu() = clickOnMenuItem(R.string.sort) + + fun applySort(sortable: Sortable, order: Order = Order.ASCENDING) { + clickSortInMenu() + val label = sortable.label + clickDialogButton(label) + val orderLabel = order.label + clickDialogButton(orderLabel) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt similarity index 92% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt index 9585ec9..0dbcdad 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt @@ -1,7 +1,7 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt new file mode 100644 index 0000000..e5f4a2f --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt @@ -0,0 +1,142 @@ +package com.appttude.h_mal.farmr.ui.tests + +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTest +import com.appttude.h_mal.farmr.ui.MainActivity +import com.appttude.h_mal.farmr.ui.robots.addScreen +import com.appttude.h_mal.farmr.ui.robots.filterScreen +import com.appttude.h_mal.farmr.ui.robots.homeScreen +import com.appttude.h_mal.farmr.ui.robots.viewScreen +import com.appttude.h_mal.farmr.utils.ID +import org.junit.Test + +class ShiftTests : BaseTest(MainActivity::class.java) { + + override fun afterLaunch() { + super.afterLaunch() + addRandomShifts() + + // Content resolver hard to mock + // Dirty technique to have a populated list + homeScreen { + clickFab() + navigateBack() + } + } + + override fun testFinished() { + super.testFinished() + clearDataBase() + clearPrefs() + } + + // Add a shift successfully + @Test + fun openAddScreen_addNewShift_newShiftCreated() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.HOURLY) + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(10f) + assertDuration("2.0 hours") + assertTotalPay("£20.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Edit a shift successfully + @Test + fun test2() { + homeScreen { + clickOnEdit(0) + } + addScreen { + setDescription("Edited this shift") + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(20f) + assertDuration("2.0 hours") + assertTotalPay("£40.00") + submit() + } + homeScreen { + clickOnItemWithText("Edited this shift") + } + viewScreen { + matchDescription("Edited this shift") + matchDuration("2 Hours 0 Minutes (+ 30 minutes break)") + matchTotalPay("2.0 Hours @ £20.00 per Hour\nEquals: £40.00") + } + } + + // filter the list with date from + @Test + fun test3() { + homeScreen { + applySort(Sortable.TYPE, Order.DESCENDING) + clickOnItemAtPosition(0) + viewScreen { + matchDescription("Day five") + matchShiftType(ShiftType.PIECE) + } + } + } + + // filter the list with date to + @Test + fun test4() { + homeScreen { + clickFilterInMenu() + } + filterScreen { + setDateIn(2023,8,3) + setDateOut(2023,8,6) + submit() + } + homeScreen { + clickOnItemAtPosition(0) + } + } + + // Add a shift as piece rate + @Test + fun test5() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.PIECE) + setRateOfPay(10f) + setUnits(1f) + assertTotalPay("£10.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Validate the details screen + @Test + fun test6() { + } + + // filter, sort, order and then reset + @Test + fun test7() { + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt new file mode 100644 index 0000000..ad69acd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt @@ -0,0 +1 @@ +package com.appttude.h_mal.farmr.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt new file mode 100644 index 0000000..0e05bb6 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt @@ -0,0 +1,103 @@ +package com.appttude.h_mal.farmr.ui.utils + +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType + +fun getShifts() = listOf( + Shift( + ShiftType.HOURLY, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.HOURLY, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.PIECE, + "Day five", + "2023-08-05", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day six", + "2023-08-06", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day seven", + "2023-08-07", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day eight", + "2023-08-08", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ) +) \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt similarity index 98% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt index e008cfe..a2ebc7a 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui.utils +package com.appttude.h_mal.farmr.ui.utils import android.os.SystemClock.sleep import android.view.View diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt similarity index 94% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt index 2a8c143..d398697 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui.utils +package com.appttude.h_mal.farmr.ui.utils import androidx.lifecycle.LiveData import androidx.lifecycle.Observer diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt new file mode 100644 index 0000000..055e5f3 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt @@ -0,0 +1,31 @@ +package com.appttude.h_mal.farmr.base + +import android.app.Application +import com.appttude.h_mal.farmr.data.RepositoryImpl +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +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 + +abstract class BaseApplication() : Application(), KodeinAware { + + // Kodein creation of modules to be retrieve within the app + override val kodein = Kodein.lazy { + import(androidXModule(this@BaseApplication)) + + bind() from singleton { createDatabase() } + bind() from singleton { createPrefs() } + bind() from singleton { RepositoryImpl(instance(), instance()) } + + bind() from provider { ApplicationViewModelFactory(instance()) } + } + + abstract fun createDatabase(): LegacyDatabase + abstract fun createPrefs(): PreferenceProvider +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt index 9fa0a96..c0195fe 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt @@ -3,12 +3,13 @@ package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.net.Uri import android.provider.BaseColumns +import com.appttude.h_mal.farmr.BuildConfig /** * Created by h_mal on 26/12/2017. */ object ShiftsContract { - const val CONTENT_AUTHORITY = "com.appttude.h_mal.farmr" + const val CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY") const val PATH_SHIFTS = "shifts" diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt index 403d5e3..11481c1 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -62,4 +62,8 @@ class PreferenceProvider( ) } + fun clearPrefs() { + preference.edit().clear().apply() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt index 0e1532f..3b0cd5f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -1,11 +1,10 @@ package com.appttude.h_mal.farmr.di -import android.app.Application +import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.data.RepositoryImpl import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory -import com.appttude.h_mal.farmr.viewmodel.MainViewModel import org.kodein.di.Kodein import org.kodein.di.KodeinAware import org.kodein.di.android.x.androidXModule @@ -14,15 +13,12 @@ import org.kodein.di.generic.instance import org.kodein.di.generic.provider import org.kodein.di.generic.singleton -class ShiftApplication: Application(), KodeinAware { - // Kodein creation of modules to be retrieve within the app - override val kodein = Kodein.lazy { - import(androidXModule(this@ShiftApplication)) +class ShiftApplication: BaseApplication() { - bind() from singleton { LegacyDatabase(contentResolver) } - bind() from singleton { PreferenceProvider(this@ShiftApplication) } - bind() from singleton { RepositoryImpl(instance(), instance()) } - - bind() from provider { ApplicationViewModelFactory(instance()) } + override fun createDatabase(): LegacyDatabase { + return LegacyDatabase(contentResolver) } -} \ No newline at end of file + + override fun createPrefs() = PreferenceProvider(this) +} + diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt index f027a92..609fd20 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -11,5 +11,9 @@ enum class Sortable(val label: String) { companion object { val entries = Sortable.values() + + fun getEnumByType(label: String): Sortable { + return Sortable.values().first { it.label == label } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index c0a09b9..889198e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -18,6 +18,7 @@ import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.ID import com.appttude.h_mal.farmr.utils.createDialog import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.popBackStack @@ -158,8 +159,8 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ mUnits = units } } - mPayRateEditText.setText(rateOfPay.formatToTwoDpString()) - mTotalPayTextView.text = totalPay.formatToTwoDpString() + mPayRateEditText.setText(rateOfPay.formatAsCurrencyString()) + mTotalPayTextView.text = totalPay.formatAsCurrencyString() calculateTotalPay() } @@ -267,7 +268,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ (mUnits ?: 0f) * mPayRate } } - mTotalPayTextView.text = total.formatToTwoDpString() + mTotalPayTextView.text = total.formatAsCurrencyString() } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index e0f1cbd..09592b9 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -155,7 +155,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setSingleChoiceItems( groupName, checkedItem - ) { p0, p1 -> sort = Sortable.valueOf(groupName[p1]) } + ) { p0, p1 -> sort = Sortable.getEnumByType(groupName[p1]) } .setPositiveButton("Ascending") { dialog, id -> viewModel.setSortAndOrder(sort) dialog.dismiss() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index 5310bdb..087320e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.View import android.widget.RelativeLayout import androidx.core.app.ActivityOptionsCompat @@ -16,8 +17,7 @@ class SplashScreen : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_splash) - val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle() - val relativeLayout = findViewById(R.id.splash_layout) as RelativeLayout + val i = Intent(this@SplashScreen, MainActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK) Handler().postDelayed({ @@ -27,11 +27,11 @@ class SplashScreen : Activity() { startActivity(i) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) // finish(); - }, SPLASH_TIME_OUT.toLong()) + }, SPLASH_TIME_OUT) } companion object { // Splash screen timer - private const val SPLASH_TIME_OUT = 2000 + const val SPLASH_TIME_OUT: Long = 2000 } } \ No newline at end of file From e0d030698ce8205942393c94bf2b0d1865b299d0 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 16:12:50 +0100 Subject: [PATCH 10/15] - mid commit --- Gemfile | 3 ++ app/build.gradle | 15 +++++++ app/src/main/AndroidManifest.xml | 2 +- .../farmr/data/legacydb/ShiftsContract.kt | 2 +- fastlane/Appfile | 2 + fastlane/Fastfile | 29 ++++++++++++++ fastlane/README.md | 40 +++++++++++++++++++ 7 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 Gemfile create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile create mode 100644 fastlane/README.md diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/app/build.gradle b/app/build.gradle index 3bba2f4..e2f23d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,12 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'kotlin-kapt' +def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") +def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD") +def relKeyAlias = System.getenv("RELEASE_KEY_ALIAS") + +def keystorePath = System.getenv('PWD') + "/app/keystore.jks" +def keystore = file(keystorePath).exists() ? file(keystorePath) : null android { compileSdkVersion 31 defaultConfig { @@ -13,8 +19,17 @@ android { testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true } + signingConfigs { + release { + storePassword relStorePassword + keyPassword relKeyPassword + keyAlias relKeyAlias + storeFile keystore + } + } buildTypes { release { + signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b13a45..6fc2773 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,7 +40,7 @@ Date: Tue, 29 Aug 2023 18:42:57 +0100 Subject: [PATCH 11/15] - mid commit --- .idea/codeStyles/Project.xml | 113 ------------------ .idea/kotlinc.xml | 6 - app/build.gradle | 2 +- .../h_mal/farmr/ui/FragmentAddItem.kt | 2 +- 4 files changed, 2 insertions(+), 121 deletions(-) delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/kotlinc.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index ae78c11..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
-
-
\ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index b1077fb..0000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index e2f23d7..e50fec6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD") def relKeyAlias = System.getenv("RELEASE_KEY_ALIAS") -def keystorePath = System.getenv('PWD') + "/app/keystore.jks" +def keystorePath = "/keystore.jks" def keystore = file(keystorePath).exists() ? file(keystorePath) : null android { compileSdkVersion 31 diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index 889198e..1b9caf5 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -26,7 +26,6 @@ import com.appttude.h_mal.farmr.utils.setDatePicker import com.appttude.h_mal.farmr.utils.setTimePicker import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.utils.validateField -import com.appttude.h_mal.farmr.viewmodel.MainViewModel import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), @@ -120,6 +119,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ private fun setupViewAfterViewCreated() { id = arguments?.getLong(ID) + wholeView.hide() val title = when (arguments?.containsKey(ID)) { true -> { From e6e3a72adff0020404a266ead19fbfd71c74c04b Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 19:13:01 +0100 Subject: [PATCH 12/15] - lint checks completed --- .idea/kotlinc.xml | 6 + .../farmr/data/legacydb/ShiftProvider.kt | 117 ++++++++++-------- .../appttude/h_mal/farmr/model/Sortable.kt | 2 - .../appttude/h_mal/farmr/ui/FragmentMain.kt | 31 +++-- .../h_mal/farmr/ui/ShiftListAdapter.kt | 6 +- .../appttude/h_mal/farmr/ui/SplashScreen.kt | 8 +- .../appttude/h_mal/farmr/utils/ViewUtils.kt | 8 +- .../h_mal/farmr/viewmodel/MainViewModel.kt | 2 +- 8 files changed, 95 insertions(+), 85 deletions(-) create mode 100644 .idea/kotlinc.xml diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..b1077fb --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt index 87ad3d8..727fe1f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt @@ -19,22 +19,24 @@ class ShiftProvider : ContentProvider() { return true } - override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, - sortOrder: String?): Cursor? { - var selection = selection - var selectionArgs = selectionArgs + override fun query( + uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, + sortOrder: String? + ): Cursor { val database = mDbHelper!!.readableDatabase - val cursor: Cursor - val match = sUriMatcher.match(uri) - when (match) { - SHIFTS -> cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder) + val cursor: Cursor = when (sUriMatcher.match(uri)) { + SHIFTS -> database.query( + ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, + null, null, sortOrder + ) SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + database.query( + ShiftsEntry.TABLE_NAME, projection, mSelection, mSelectionArgs, + null, null, sortOrder + ) } else -> throw IllegalArgumentException("Cannot query $uri") @@ -44,26 +46,25 @@ class ShiftProvider : ContentProvider() { } override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { - val match = sUriMatcher.match(uri) - return when (match) { + return when (sUriMatcher.match(uri)) { SHIFTS -> insertShift(uri, contentValues) else -> throw IllegalArgumentException("Insertion is not supported for $uri") } } private fun insertShift(uri: Uri, values: ContentValues?): Uri? { - val description = values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - ?: throw IllegalArgumentException("Description required") - val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) - ?: throw IllegalArgumentException("Date required") - val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - ?: throw IllegalArgumentException("Time In required") - val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - ?: throw IllegalArgumentException("Time Out required") + values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) + ?: throw IllegalArgumentException("Description required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) + ?: throw IllegalArgumentException("Date required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) + ?: throw IllegalArgumentException("Time In required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) + ?: throw IllegalArgumentException("Time Out required") val duration = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_DURATION) require(duration >= 0) { "Duration cannot be negative" } - val shiftType = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE) - ?: throw IllegalArgumentException("Shift type required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE) + ?: throw IllegalArgumentException("Shift type required") val shiftUnits = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_UNIT) require(shiftUnits >= 0) { "Units cannot be negative" } val payRate = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_PAYRATE) @@ -82,43 +83,47 @@ class ShiftProvider : ContentProvider() { return ContentUris.withAppendedId(uri, id) } - override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, - selectionArgs: Array?): Int { - var selection = selection - var selectionArgs = selectionArgs - val match = sUriMatcher.match(uri) - return when (match) { + override fun update( + uri: Uri, contentValues: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + return when (sUriMatcher.match(uri)) { SHIFTS -> updateShift(uri, contentValues, selection, selectionArgs) SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - updateShift(uri, contentValues, selection, selectionArgs) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + updateShift(uri, contentValues, mSelection, mSelectionArgs) } else -> throw IllegalArgumentException("Update is not supported for $uri") } } - private fun updateShift(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + private fun updateShift( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { if (values!!.containsKey(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) { - val description = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - ?: throw IllegalArgumentException("description required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) + ?: throw IllegalArgumentException("description required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_DATE)) { - val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) - ?: throw IllegalArgumentException("date required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) + ?: throw IllegalArgumentException("date required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) { - val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - ?: throw IllegalArgumentException("time in required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) + ?: throw IllegalArgumentException("time in required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) { - val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - ?: throw IllegalArgumentException("time out required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) + ?: throw IllegalArgumentException("time out required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_BREAK)) { - val breaks = values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK) - ?: throw IllegalArgumentException("break required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK) + ?: throw IllegalArgumentException("break required") } if (values.size() == 0) { return 0 @@ -132,17 +137,15 @@ class ShiftProvider : ContentProvider() { } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - var selection = selection - var selectionArgs = selectionArgs val database = mDbHelper!!.writableDatabase - val rowsDeleted: Int - val match = sUriMatcher.match(uri) - when (match) { - SHIFTS -> rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + val rowsDeleted: Int = when (sUriMatcher.match(uri)) { + SHIFTS -> database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + + database.delete(ShiftsEntry.TABLE_NAME, mSelection, mSelectionArgs) } else -> throw IllegalArgumentException("Deletion is not supported for $uri") @@ -169,7 +172,11 @@ class ShiftProvider : ContentProvider() { init { sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS, SHIFTS) - sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS + "/#", SHIFT_ID) + sUriMatcher.addURI( + ShiftsContract.CONTENT_AUTHORITY, + ShiftsContract.PATH_SHIFTS + "/#", + SHIFT_ID + ) } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt index 609fd20..bb2bbf7 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -10,8 +10,6 @@ enum class Sortable(val label: String) { TOTALPAY("Total Pay"); companion object { - val entries = Sortable.values() - fun getEnumByType(label: String): Sortable { return Sortable.values().first { it.label == label } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index 09592b9..8e420e0 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -95,7 +95,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr AlertDialog.Builder(context) .setTitle("Help & Support:") .setView(R.layout.dialog_layout) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> arg0.dismiss() } + .setPositiveButton(android.R.string.ok) { arg0, _ -> arg0.dismiss() } .create().show() return true } @@ -120,12 +120,11 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr AlertDialog.Builder(context) .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() } + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> exportData() } .create().show() } else { - Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT) - .show() + displayToast("Storage permissions required") } return true } @@ -134,7 +133,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr AlertDialog.Builder(context) .setTitle("Info:") .setMessage(viewModel.getInformation()) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> + .setPositiveButton(android.R.string.ok) { arg0, _ -> arg0.dismiss() }.create().show() return true @@ -144,7 +143,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } private fun sortData() { - val groupName = Sortable.entries.map { it.label }.toTypedArray() + val groupName = Sortable.values().map { it.label }.toTypedArray() var sort = Sortable.ID val sortAndOrder = viewModel.getSortAndOrder() @@ -155,11 +154,11 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setSingleChoiceItems( groupName, checkedItem - ) { p0, p1 -> sort = Sortable.getEnumByType(groupName[p1]) } - .setPositiveButton("Ascending") { dialog, id -> + ) { _, p1 -> sort = Sortable.getEnumByType(groupName[p1]) } + .setPositiveButton("Ascending") { dialog, _ -> viewModel.setSortAndOrder(sort) dialog.dismiss() - }.setNegativeButton("Descending") { dialog, id -> + }.setNegativeButton("Descending") { dialog, _ -> viewModel.setSortAndOrder(sort, Order.DESCENDING) dialog.dismiss() } @@ -210,25 +209,25 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr super.onRequestPermissionsResult(requestCode, permissions, grantResults) println("request code$requestCode") if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { - if (grantResults.size > 0 + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED ) { exportDialog() } else { - Toast.makeText(context, "Storage Permissions denied", Toast.LENGTH_SHORT).show() + displayToast("Storage Permissions denied") } } } - fun exportDialog() { + private fun exportDialog() { AlertDialog.Builder(context) .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() }.create().show() + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> exportData() }.create().show() } - fun checkStoragePermissions(activity: Activity?): Boolean { + private fun checkStoragePermissions(activity: Activity?): Boolean { var status = false val permission = ActivityCompat.checkSelfPermission( activity!!, diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt index 59d8987..5151661 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt @@ -1,5 +1,6 @@ package com.appttude.h_mal.farmr.ui +import android.annotation.SuppressLint import android.app.AlertDialog import android.os.Bundle import android.view.ViewGroup @@ -28,6 +29,7 @@ class ShiftListAdapter( return BaseRecyclerAdapter.CurrentViewHolder(currentViewHolder) } + @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: BaseRecyclerAdapter.CurrentViewHolder, position: Int) { val view = holder.itemView val data = getItem(position) @@ -90,8 +92,8 @@ class ShiftListAdapter( view.setOnLongClickListener { AlertDialog.Builder(it.context) .setMessage("Are you sure you want to delete") - .setPositiveButton("delete") { dialog, id -> longPressCallback.invoke(data.id) } - .setNegativeButton("cancel") { dialog, id -> + .setPositiveButton("delete") { _, _ -> longPressCallback.invoke(data.id) } + .setNegativeButton("cancel") { dialog, _ -> dialog?.dismiss() } .create().show() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index 087320e..f0fefb2 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -1,5 +1,6 @@ package com.appttude.h_mal.farmr.ui +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle @@ -13,6 +14,7 @@ import com.appttude.h_mal.farmr.R /** * Created by h_mal on 27/06/2017. */ +@SuppressLint("CustomSplashScreen") class SplashScreen : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -20,13 +22,9 @@ class SplashScreen : Activity() { val i = Intent(this@SplashScreen, MainActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK) - Handler().postDelayed({ - // This method will be executed once the timer is over - // Start your app main activity -// startActivity(i,bundle); + Handler(Looper.getMainLooper()).postDelayed({ startActivity(i) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - // finish(); }, SPLASH_TIME_OUT) } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt index 659b798..b863b69 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt @@ -161,12 +161,12 @@ fun EditText.setDatePicker(onSelected: (String) -> Unit) { } val mDatePicker = DatePickerDialog( (this.context), - { datepicker, selectedyear, selectedmonth, selectedday -> - var currentMonth = selectedmonth - val dateString = StringBuilder().append(selectedyear).append("-") + { _, selectedYear, selectedMonth, selectedDay -> + var currentMonth = selectedMonth + val dateString = StringBuilder().append(selectedYear).append("-") .append(String.format("%02d", (currentMonth + 1.also { currentMonth = it }))) .append("-") - .append(String.format("%02d", selectedday)) + .append(String.format("%02d", selectedDay)) .toString() setText(dateString) onSelected.invoke(dateString) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index 7a99874..f07cb9d 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -239,7 +239,7 @@ class MainViewModel( val data = shiftLiveData.value!!.applyFilters() .sortList(sortAndOrder.first, sortAndOrder.second) var currentRow = 0 - val cells = data.mapIndexed { index, shift -> + val cells = data.map { shift -> currentRow += 1 listOf( Label(0, currentRow, shift.id.toString()), From b81d00528858cb2a41586602656dbb11e8d02504 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 20:46:10 +0100 Subject: [PATCH 13/15] - lint checks completed --- .../h_mal/farmr/viewmodel/InfoViewModel.kt | 1 - .../farmr/viewmodel/InfoViewModelTest.kt | 93 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt index dea7585..025829e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt @@ -35,6 +35,5 @@ class InfoViewModel( stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") } return stringBuilder.toString() - } } \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt new file mode 100644 index 0000000..3fb73c0 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt @@ -0,0 +1,93 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.os.Bundle +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.ID +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.ArgumentMatchers.anyLong +import kotlin.test.assertIs + +class InfoViewModelTest : ShiftViewModelTest() { + + @Test + fun retrieveData_validBundleAndId_successfulRetrieval() { + // Arrange + val id = anyLong() + val shift = mockk() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(shift) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(bundle) + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + retrieveCurrentData(), + shift + ) + } + + @Test + fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() { + // Arrange + val id = anyLong() + val shift = mockk() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(shift) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(null) + + // Assert + assertEquals( + retrieveCurrentError(), + "Failed to retrieve shift" + ) + } + + @Test + fun retrieveData_validBundleNoShift_successfulRetrieval() { + // Arrange + val id = anyLong() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(null) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(bundle) + + // Assert + assertEquals( + retrieveCurrentError(), + "Failed to retrieve shift" + ) + } + + @Test + fun buildDurationSummary_validHourlyShift_successfulRetrieval() { + // Arrange + val shift = getShifts()[0] + val shiftWithBreak = getShifts()[3] + + // Act + val summary = viewModel.buildDurationSummary(shift) + val summaryWithBreak = viewModel.buildDurationSummary(shiftWithBreak) + + // Assert + assertEquals( + "1 Hours 0 Minutes ", + summary + ) + assertEquals( + "1 Hours 0 Minutes (+ 30 minutes break)", + summaryWithBreak + ) + } + +} \ No newline at end of file From 91ab3aab43920578d15e118d194da5cd359223ff Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 20:53:02 +0100 Subject: [PATCH 14/15] - Local unit tests added - Optomized imports --- .../h_mal/farmr/data/ShiftProviderTest.kt | 2 +- .../com/appttude/h_mal/farmr/ui/BaseTest.kt | 3 - .../farmr/ui/robots/FilterScreenRobot.kt | 2 +- .../h_mal/farmr/ui/robots/HomeScreenRobot.kt | 1 - .../farmr/ui/robots/ViewItemScreenRobot.kt | 2 +- .../h_mal/farmr/ui/tests/ShiftTests.kt | 1 - .../appttude/h_mal/farmr/base/BaseFragment.kt | 2 - .../h_mal/farmr/base/BaseViewModel.kt | 1 - .../farmr/data/legacydb/LegacyDatabase.kt | 2 - .../farmr/data/legacydb/ShiftsContract.kt | 1 - .../h_mal/farmr/di/ShiftApplication.kt | 9 -- .../com/appttude/h_mal/farmr/model/Shift.kt | 3 - .../appttude/h_mal/farmr/ui/FragmentMain.kt | 2 - .../h_mal/farmr/ui/FurtherInfoFragment.kt | 1 - .../appttude/h_mal/farmr/ui/MainActivity.kt | 7 -- .../appttude/h_mal/farmr/ui/SplashScreen.kt | 3 - .../appttude/h_mal/farmr/utils/ViewUtils.kt | 2 - .../h_mal/farmr/viewmodel/MainViewModel.kt | 1 - .../h_mal/farmr/viewmodel/ShiftViewModel.kt | 2 +- .../h_mal/farmr/data/RepositoryImplTest.kt | 1 - .../appttude/h_mal/farmr/utils/testUtils.kt | 112 +++++++++++++++++- .../farmr/viewmodel/MainViewModelTest.kt | 111 +---------------- 22 files changed, 116 insertions(+), 155 deletions(-) diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt index 7a11af7..31f500e 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt @@ -3,6 +3,7 @@ package com.appttude.h_mal.farmr.data import android.content.ContentResolver import android.content.ContentValues import androidx.test.rule.provider.ProviderTestRule +import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.CONTENT_AUTHORITY import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE @@ -16,7 +17,6 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID -import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull import org.junit.After diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt index 0536254..8d4ebb8 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt @@ -6,9 +6,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View -import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario -import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -18,7 +16,6 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.appttude.h_mal.farmr.application.TestAppClass -import com.appttude.h_mal.farmr.di.ShiftApplication import com.appttude.h_mal.farmr.ui.utils.getShifts import kotlinx.coroutines.runBlocking import org.hamcrest.Matcher diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt index 5907ac6..acd6f63 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt @@ -1,8 +1,8 @@ package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } class FilterScreenRobot : BaseTestRobot() { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt index 4584b4a..81bf480 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt @@ -1,6 +1,5 @@ package com.appttude.h_mal.farmr.ui.robots -import androidx.test.espresso.matcher.RootMatchers.isDialog import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder import com.appttude.h_mal.farmr.model.Order diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt index 0dbcdad..b56d799 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt @@ -1,8 +1,8 @@ package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } class ViewItemScreenRobot : BaseTestRobot() { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt index e5f4a2f..a7decde 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt @@ -9,7 +9,6 @@ import com.appttude.h_mal.farmr.ui.robots.addScreen import com.appttude.h_mal.farmr.ui.robots.filterScreen import com.appttude.h_mal.farmr.ui.robots.homeScreen import com.appttude.h_mal.farmr.ui.robots.viewScreen -import com.appttude.h_mal.farmr.utils.ID import org.junit.Test class ShiftTests : BaseTest(MainActivity::class.java) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt index 3a25089..a74edba 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -4,14 +4,12 @@ import android.os.Bundle import android.view.View import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment -import androidx.fragment.app.createViewModelLazy import androidx.lifecycle.ViewModelLazy import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getGenericClassAt import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import org.kodein.di.KodeinAware -import org.kodein.di.android.kodein import org.kodein.di.android.x.kodein import org.kodein.di.generic.instance import kotlin.properties.Delegates diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt index 40c0b5c..8ff7475 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.appttude.h_mal.farmr.model.ViewState -import java.lang.Exception open class BaseViewModel: ViewModel() { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt index 57e019e..729ea9a 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt @@ -3,7 +3,6 @@ package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues -import android.content.Context import android.database.Cursor import android.net.Uri import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK @@ -19,7 +18,6 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.model.Shift -import com.appttude.h_mal.farmr.model.ShiftType class LegacyDatabase(private val resolver: ContentResolver) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt index 51d1976..9fa0a96 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt @@ -3,7 +3,6 @@ package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.net.Uri import android.provider.BaseColumns -import com.appttude.h_mal.farmr.BuildConfig /** * Created by h_mal on 26/12/2017. diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt index 3b0cd5f..cc4b998 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -1,17 +1,8 @@ package com.appttude.h_mal.farmr.di import com.appttude.h_mal.farmr.base.BaseApplication -import com.appttude.h_mal.farmr.data.RepositoryImpl import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider -import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory -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 ShiftApplication: BaseApplication() { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 2f87159..4df1de5 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -1,10 +1,7 @@ package com.appttude.h_mal.farmr.model -import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.utils.calculateDuration -import com.appttude.h_mal.farmr.utils.convertTimeStringToHourMinutesPair import com.appttude.h_mal.farmr.utils.formatToTwoDp -import java.io.IOException data class Shift( val type: ShiftType, diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index 8e420e0..5e1866b 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -1,12 +1,10 @@ package com.appttude.h_mal.farmr.ui import android.Manifest -import android.annotation.SuppressLint import android.app.Activity import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager -import android.net.Uri import android.os.Bundle import android.view.MenuItem import android.view.View diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt index e536e10..be79fa7 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt @@ -12,7 +12,6 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.utils.CURRENCY import com.appttude.h_mal.farmr.utils.formatAsCurrencyString -import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.utils.show diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt index 245b54f..2bb2763 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -1,11 +1,7 @@ package com.appttude.h_mal.farmr.ui import android.Manifest -import android.R.string.cancel -import android.R.string.ok import android.app.Activity -import android.app.AlertDialog -import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.view.Menu @@ -14,10 +10,7 @@ import androidx.core.app.ActivityCompat import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseActivity -import com.appttude.h_mal.farmr.utils.createDialog import com.appttude.h_mal.farmr.utils.popBackStack -import com.appttude.h_mal.farmr.viewmodel.MainViewModel -import kotlin.system.exitProcess class MainActivity : BaseActivity() { private lateinit var toolbar: Toolbar diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index f0fefb2..5f8cfd8 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -6,9 +6,6 @@ import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper -import android.view.View -import android.widget.RelativeLayout -import androidx.core.app.ActivityOptionsCompat import com.appttude.h_mal.farmr.R /** diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt index b863b69..1e68035 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt @@ -19,9 +19,7 @@ import androidx.annotation.AnimRes import androidx.annotation.IdRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.ui.FragmentAddItem import java.util.Calendar fun View.show() { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index f07cb9d..7929615 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -22,7 +22,6 @@ import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Success -import com.appttude.h_mal.farmr.utils.CURRENCY import com.appttude.h_mal.farmr.utils.convertDateString import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.sortedByOrder diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt index 33c7ea5..abcc710 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt @@ -2,9 +2,9 @@ package com.appttude.h_mal.farmr.viewmodel import com.appttude.h_mal.farmr.base.BaseViewModel import com.appttude.h_mal.farmr.data.Repository -import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.DATE_IN import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.TYPE import com.appttude.h_mal.farmr.model.FilterStore diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt index 4c53925..4688aa6 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt @@ -10,7 +10,6 @@ import io.mockk.mockk import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyLong -import java.util.UUID import kotlin.test.assertEquals import kotlin.test.assertIs diff --git a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt index 4d64b8d..e04e3ce 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt @@ -2,9 +2,12 @@ package com.appttude.h_mal.farmr.utils import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.mockito.ArgumentMatchers import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException @@ -36,4 +39,111 @@ fun LiveData.getOrAwaitValue( fun sleep(millis: Long = 1000) { runBlocking(Dispatchers.Default) { delay(millis) } -} \ No newline at end of file +} + +fun getShifts() = listOf( + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), +) \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt index 127dab4..4ba2d24 100644 --- a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt @@ -10,17 +10,14 @@ import com.appttude.h_mal.farmr.data.prefs.TYPE import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import com.appttude.h_mal.farmr.utils.getShifts import io.mockk.every import io.mockk.mockk import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Rule import org.junit.Test -import org.mockito.ArgumentMatchers.anyFloat -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyList -import org.mockito.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.anyString import java.util.concurrent.TimeoutException import kotlin.test.assertEquals @@ -130,110 +127,4 @@ class MainViewModelTest { Pair(TYPE, type) ) - private fun getShifts() = listOf( - ShiftObject( - anyLong(), - ShiftType.HOURLY.type, - "Day one", - "2023-08-01", - "12:00", - "13:00", - 1f, - anyInt(), - anyFloat(), - 10f, - 10f - ), - ShiftObject( - anyLong(), - ShiftType.HOURLY.type, - "Day two", - "2023-08-02", - "12:00", - "13:00", - 1f, - anyInt(), - anyFloat(), - 10f, - 10f - ), - ShiftObject( - anyLong(), - ShiftType.HOURLY.type, - "Day three", - "2023-08-03", - "12:00", - "13:00", - 1f, - 30, - anyFloat(), - 10f, - 5f - ), - ShiftObject( - anyLong(), - ShiftType.HOURLY.type, - "Day four", - "2023-08-04", - "12:00", - "13:00", - 1f, - 30, - anyFloat(), - 10f, - 5f - ), - ShiftObject( - anyLong(), - ShiftType.PIECE.type, - "Day five", - "2023-08-05", - anyString(), - anyString(), - anyFloat(), - anyInt(), - 1f, - 10f, - 10f - ), - ShiftObject( - anyLong(), - ShiftType.PIECE.type, - "Day six", - "2023-08-06", - anyString(), - anyString(), - anyFloat(), - anyInt(), - 1f, - 10f, - 10f - ), - ShiftObject( - anyLong(), - ShiftType.PIECE.type, - "Day seven", - "2023-08-07", - anyString(), - anyString(), - anyFloat(), - anyInt(), - 1f, - 10f, - 10f - ), - ShiftObject( - anyLong(), - ShiftType.PIECE.type, - "Day eight", - "2023-08-08", - anyString(), - anyString(), - anyFloat(), - anyInt(), - 1f, - 10f, - 10f - ), - ) } \ No newline at end of file From 6e399994d56f82664b203ced44de4474aee9434e Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 21:18:33 +0100 Subject: [PATCH 15/15] - fastlane fix --- app/build.gradle | 4 ++-- fastlane/Appfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e50fec6..19592a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId "com.appttude.h_mal.farmr" minSdkVersion 21 targetSdkVersion 31 - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "2.0" testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true } diff --git a/fastlane/Appfile b/fastlane/Appfile index a51c5e2..116ee2e 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,2 +1,2 @@ -json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +json_key_file("google-play-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one package_name("com.appttude.h_mal.farmr") # e.g. com.krausefx.app