diff --git a/.circleci/config.yml b/.circleci/config.yml index 342196f..983441f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,7 +39,7 @@ commands: - 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 + system-image: system-images;android-31;default;x86_64 # store screenshots for failed ui tests - when: condition: on_fail @@ -120,6 +120,7 @@ workflows: only: - master - release + - /ui_test.*/ - deploy-to-playstore: context: appttude filters: @@ -127,4 +128,4 @@ workflows: only: - release requires: - - build-and-test \ No newline at end of file + - run_instrumentation_test \ No newline at end of file diff --git a/.gitignore b/.gitignore index ae7f176..be47ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ gen-external-apklibs .idea/assetWizardSettings.xml .idea/gradle.xml .idea/jarRepositories.xml +.idea/navEditor.xml # Gem/fastlane Gemfile.lock diff --git a/app/build.gradle b/app/build.gradle index 8574611..5c4fd36 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: 'androidx.navigation.safeargs' apply plugin: 'kotlin-kapt' def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") @@ -14,8 +15,8 @@ android { applicationId "com.appttude.h_mal.farmr" minSdkVersion MIN_SDK_VERSION targetSdkVersion TARGET_SDK_VERSION - versionCode 3 - versionName "2.1" + versionCode 7 + versionName "3.0" testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true javaCompileOptions { @@ -60,6 +61,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-core:$LIFECYCLE_VERSION" implementation "androidx.lifecycle:lifecycle-viewmodel:$LIFECYCLE_VERSION" implementation 'androidx.recyclerview:recyclerview:1.0.0' + / * Fragment Navigation * / + implementation "androidx.navigation:navigation-fragment-ktx:$NAVIGATION_VERSION" + implementation "androidx.navigation:navigation-ui-ktx:$NAVIGATION_VERSION" / * Unit testing * / testImplementation "junit:junit:$JUNIT_VERSION" androidTestRuntimeOnly "org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION" @@ -97,4 +101,6 @@ dependencies { implementation "org.kodein.di:kodein-di-framework-android-core:$KODEIN_VERSION" / * jxl * / implementation "net.sourceforge.jexcelapi:jxl:$JEXCEL_VERSION" + / * calendar view * / + implementation 'com.applandeo:material-calendar-view:1.7.0' } 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 151a2c1..46f19b7 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 @@ -15,6 +15,8 @@ import androidx.test.espresso.matcher.RootMatchers.withDecorView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import com.appttude.h_mal.farmr.application.TestAppClass +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitFor +import com.appttude.h_mal.farmr.ui.utils.generateShifts import com.appttude.h_mal.farmr.ui.utils.getShifts import kotlinx.coroutines.runBlocking import org.hamcrest.Matcher @@ -73,16 +75,6 @@ open class BaseTest( 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))) @@ -95,7 +87,7 @@ open class BaseTest( fun navigateBack() = Espresso.pressBack() fun addRandomShifts() { - testApp.addShiftsToDatabase(getShifts()) + testApp.addShiftsToDatabase(generateShifts()) } fun clearDataBase() = testApp.clearDatabase() diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt index a85098f..7f68f6a 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt @@ -23,6 +23,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything @@ -58,6 +59,8 @@ open class BaseTestRobot { fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) + fun scrollTo(viewId: Int): ViewInteraction = matchView(viewId).perform(ViewActions.scrollTo()) + fun clickListItem(listRes: Int, position: Int) { onData(anything()) .inAdapterView(allOf(withId(listRes))) diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/CalendarScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/CalendarScreenRobot.kt new file mode 100644 index 0000000..e8bc5ad --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/CalendarScreenRobot.kt @@ -0,0 +1,55 @@ +package com.appttude.h_mal.farmr.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.hasTextColor +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.ui.BaseTestRobot +import org.hamcrest.core.AllOf.allOf +import org.hamcrest.core.IsNot.not + +fun calendarScreen(func: CalendarScreenRobot.() -> Unit) = CalendarScreenRobot().apply { func() } +class CalendarScreenRobot : BaseTestRobot() { + + fun clickOnListItemWithText(text: String) = + clickOnRecyclerItemWithText(R.id.shifts_available_recycler, text) + + fun clickOnListItemAtPosition(position: Int) = + clickRecyclerAtPosition(R.id.shifts_available_recycler, position) + + fun clickOnEditForItem(position: Int) { + clickViewInRecyclerAtPosition( + R.id.shifts_available_recycler, + position, + R.id.imageView + ) + onView(withId(R.id.update)).perform(click()) + } + + fun clickOnDeleteForItem(position: Int) { + clickViewInRecyclerAtPosition( + R.id.shifts_available_recycler, + position, + R.id.imageView + ) + onView(withId(R.id.delete)).perform(click()) + } + + fun clickOnCalendarDay(day: Int) { + onView( + allOf( + withId(R.id.dayLabel), + not(hasTextColor(com.applandeo.materialcalendarview.R.color.nextMonthDayColor)), + withText("$day"), + isDisplayed() + ) + ).perform(click()) + } + + fun clickNextMonth() = clickButton(com.applandeo.materialcalendarview.R.id.forwardButton) + fun clickPreviousMonth() = clickButton(com.applandeo.materialcalendarview.R.id.previousButton) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FurtherInfoScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FurtherInfoScreenRobot.kt new file mode 100644 index 0000000..f9e4f7d --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FurtherInfoScreenRobot.kt @@ -0,0 +1,33 @@ +package com.appttude.h_mal.farmr.ui.robots + +import androidx.test.espresso.action.ViewActions.scrollTo +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun furtherInfoScreen(func: FurtherInfoScreenRobot.() -> Unit) = FurtherInfoScreenRobot().apply { func() } +class FurtherInfoScreenRobot : BaseTestRobot() { + fun assertShiftType(type: ShiftType) { + matchText(R.id.details_shift, type.type) + } + + fun assertDescription(details: String) = matchText(R.id.details_desc, details) + + fun assertDate(date: String) = matchText(R.id.details_date, date) + + fun assertTime(time: String) = matchText(R.id.details_time, time) + + fun assertBreak(breakSummary: String) = matchText(R.id.details_breaks, breakSummary) + + fun assertDuration(duration: String) = matchText(R.id.details_duration, duration) + + fun assertUnits(units: String) = fillEditText(R.id.details_units, units) + fun assertRateOfPay(rateOfPay: String) = matchText(R.id.details_pay_rate, rateOfPay) + + fun assertTotalPay(text: String?) = fillEditText(R.id.details_totalpay, text) + + fun update() { + matchView(R.id.details_edit).perform(scrollTo()) + clickButton(R.id.details_edit) + } +} \ No newline at end of file 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 81bf480..39adc86 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,7 +1,9 @@ package com.appttude.h_mal.farmr.ui.robots +import androidx.test.espresso.Espresso +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers 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 @@ -9,9 +11,6 @@ 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) @@ -25,4 +24,16 @@ class HomeScreenRobot : BaseTestRobot() { val orderLabel = order.label clickDialogButton(orderLabel) } + + fun clickTab(tab: Tab) { + val id = when (tab) { + Tab.LIST -> R.id.nav_list + Tab.CALENDAR -> R.id.nav_calendar + } + Espresso.onView(ViewMatchers.withId(id)).perform(ViewActions.click()) + } + + enum class Tab { + LIST, CALENDAR + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ListScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ListScreenRobot.kt new file mode 100644 index 0000000..101615a --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ListScreenRobot.kt @@ -0,0 +1,54 @@ +package com.appttude.h_mal.farmr.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.hasChildCount +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.ui.BaseTestRobot +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitFor + +fun listScreen(func: ListScreenRobot.() -> Unit) = ListScreenRobot().apply { func() } +class ListScreenRobot : BaseTestRobot() { + + fun clickOnItemWithText(text: String) = + clickOnRecyclerItemWithText(R.id.list_item_view, text) + + fun clickOnItemAtPosition(position: Int) = + clickRecyclerAtPosition(R.id.list_item_view, position) + + fun clickOnEditForItem(position: Int) { + clickViewInRecyclerAtPosition( + R.id.list_item_view, + position, + R.id.imageView + ) + waitFor(800) + EspressoHelper.waitForView(withText("Update Shift")).perform(click()) + } + + fun clickOnDeleteForItem(position: Int) { + clickViewInRecyclerAtPosition( + R.id.list_item_view, + position, + R.id.imageView + ) + waitFor(800) + EspressoHelper.waitForView(withText("Delete Shift")).perform(click()) + } + + fun confirmDeleteItemOnDialog() { + onView(withText("delete")) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()) + } + + fun assertListCount(count: Int) = + matchView(R.id.list_item_view).check(matches(hasChildCount(count))) +} \ No newline at end of file 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 b56d799..b549e12 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 @@ -23,9 +23,9 @@ class ViewItemScreenRobot : BaseTestRobot() { 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 matchBreakTime(mins: Int) = matchText(R.id.details_breaks, "$mins mins") + fun matchUnits(units: Float) = matchText(R.id.details_units, units.toString()) + fun matchRateOfPay(rateOfPay: Float) = matchText(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) diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/DummyShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/DummyShiftTests.kt new file mode 100644 index 0000000..409e026 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/DummyShiftTests.kt @@ -0,0 +1,31 @@ +package com.appttude.h_mal.farmr.ui.tests + +import com.appttude.h_mal.farmr.ui.BaseTest +import com.appttude.h_mal.farmr.ui.MainActivity +import com.appttude.h_mal.farmr.ui.robots.homeScreen +import org.junit.Ignore +import org.junit.Test + +@Ignore +class DummyShiftTests : 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() + } + } + + // Add a shift successfully + @Test + fun openAddScreen_addNewHourlyShift_assertShiftDetail() { + homeScreen { + clickFab() + } + } +} \ No newline at end of file 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 a7decde..272e1d3 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 @@ -1,15 +1,25 @@ package com.appttude.h_mal.farmr.ui.tests +import androidx.test.espresso.action.ViewActions +import com.appttude.h_mal.farmr.R 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.HomeScreenRobot import com.appttude.h_mal.farmr.ui.robots.addScreen +import com.appttude.h_mal.farmr.ui.robots.calendarScreen 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.listScreen import com.appttude.h_mal.farmr.ui.robots.viewScreen +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitFor +import org.junit.Ignore import org.junit.Test +import java.util.Calendar +import java.util.Calendar.MONTH +import java.util.Calendar.YEAR class ShiftTests : BaseTest(MainActivity::class.java) { @@ -33,7 +43,7 @@ class ShiftTests : BaseTest(MainActivity::class.java) { // Add a shift successfully @Test - fun openAddScreen_addNewShift_newShiftCreated() { + fun openAddScreen_addNewHourlyShift_assertShiftDetail() { homeScreen { clickFab() } @@ -49,16 +59,25 @@ class ShiftTests : BaseTest(MainActivity::class.java) { assertTotalPay("£20.00") submit() } - homeScreen { + listScreen { clickOnItemWithText("This is a description") } + viewScreen { + matchDescription("This is a description") + matchDate("2023-02-11") + matchShiftType(ShiftType.HOURLY) + matchTime("12:00", "14:30") + matchBreakTime(30) + matchRateOfPay(10.0f) + matchDuration("2 Hours 0 Minutes (+ 30 minutes break)") + matchTotalPay("2.0 Hours @ £10.00 per Hour\nEquals: £20.00") + } } - // Edit a shift successfully @Test - fun test2() { - homeScreen { - clickOnEdit(0) + fun editShift_newDetailsAdded_assertShiftDetail() { + listScreen { + clickOnEditForItem(0) } addScreen { setDescription("Edited this shift") @@ -70,7 +89,7 @@ class ShiftTests : BaseTest(MainActivity::class.java) { assertTotalPay("£40.00") submit() } - homeScreen { + listScreen { clickOnItemWithText("Edited this shift") } viewScreen { @@ -80,12 +99,13 @@ class ShiftTests : BaseTest(MainActivity::class.java) { } } - // filter the list with date from @Test - fun test3() { + fun applySort_listIsSorted_assertShiftsSortedCorrectly() { homeScreen { applySort(Sortable.TYPE, Order.DESCENDING) - clickOnItemAtPosition(0) + listScreen { + clickOnItemAtPosition(0) + } viewScreen { matchDescription("Day five") matchShiftType(ShiftType.PIECE) @@ -93,25 +113,26 @@ class ShiftTests : BaseTest(MainActivity::class.java) { } } - // filter the list with date to @Test - fun test4() { + fun applyDateBetweenFilterAndClear_listIsFilteredByDate_assertFilteredResultsCorrectly() { homeScreen { clickFilterInMenu() } filterScreen { - setDateIn(2023,8,3) - setDateOut(2023,8,6) + val calendar = Calendar.getInstance() + val year = calendar.get(YEAR) + val month = calendar.get(MONTH) + 1 + setDateIn(year, month, 3) + setDateOut(year, month, 6) submit() } - homeScreen { - clickOnItemAtPosition(0) + listScreen { + assertListCount(4) } } - // Add a shift as piece rate @Test - fun test5() { + fun openAddScreen_addNewPieceShift_assertShiftDetail() { homeScreen { clickFab() } @@ -124,18 +145,44 @@ class ShiftTests : BaseTest(MainActivity::class.java) { assertTotalPay("£10.00") submit() } - homeScreen { + listScreen { clickOnItemWithText("This is a description") } + viewScreen { + matchDescription("This is a description") + matchDate("2023-02-11") + matchShiftType(ShiftType.PIECE) + matchUnits(1f) + matchRateOfPay(10.0f) + matchTotalPay("1.0 Units @ £10.00 per Unit\nEquals: £10.00") + } } - // Validate the details screen + @Ignore("Fails in circleci - device size") @Test - fun test6() { + fun openCalendarTab_clickOnFirstActiveDay_assertShiftDetails() { + homeScreen { + clickTab(HomeScreenRobot.Tab.CALENDAR) + } + calendarScreen { + clickOnCalendarDay(1) + scrollTo(R.id.shifts_available_recycler) + clickOnListItemAtPosition(0) + } + viewScreen { + matchDate("2023-09-01") + } } - // filter, sort, order and then reset @Test - fun test7() { + fun deleteShift_confirmDelete_assertShiftDeleted() { + listScreen { + clickOnDeleteForItem(0) + confirmDeleteItemOnDialog() + clickOnItemAtPosition(0) + } + viewScreen { + matchDescription("Day two") + } } } \ No newline at end of file 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 index 0e05bb6..423cdad 100644 --- 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 @@ -2,6 +2,13 @@ package com.appttude.h_mal.farmr.ui.utils import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.DATE_FORMAT +import com.appttude.h_mal.farmr.utils.TIME_FORMAT +import com.appttude.h_mal.farmr.utils.getTimeString +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Calendar.DAY_OF_MONTH +import java.util.Locale fun getShifts() = listOf( Shift( @@ -100,4 +107,113 @@ fun getShifts() = listOf( 10f, 10f ) -) \ No newline at end of file +) + +fun Calendar.setDayAndGetDateString(day: Int): String { + set(Calendar.DAY_OF_MONTH, day) + val format = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()) + return format.format(time) +} + +fun generateShifts(): List { + val calendar: Calendar = Calendar.getInstance() + + return listOf( + Shift( + ShiftType.HOURLY, + "Day one", + calendar.setDayAndGetDateString(1), + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day two", + calendar.setDayAndGetDateString(2), + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day three", + calendar.setDayAndGetDateString(3), + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.HOURLY, + "Day four", + calendar.setDayAndGetDateString(4), + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.PIECE, + "Day five", + calendar.setDayAndGetDateString(5), + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day six", + calendar.setDayAndGetDateString(6), + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day seven", + calendar.setDayAndGetDateString(7), + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day eight", + calendar.setDayAndGetDateString(8), + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ) + ) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt index a2ebc7a..b0e3395 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt @@ -4,11 +4,13 @@ import android.os.SystemClock.sleep import android.view.View import android.widget.CheckBox import android.widget.Checkable +import androidx.test.espresso.Espresso 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 import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.util.TreeIterables import org.hamcrest.BaseMatcher @@ -120,4 +122,14 @@ object EspressoHelper { throw Exception("Error finding a view matching $viewMatcher") } + + fun waitFor(delay: Long) { + onView(isRoot()).perform(object : ViewAction { + override fun getConstraints(): Matcher = isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + }) + } } \ 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 a74edba..f937d1a 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 @@ -29,13 +29,6 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) 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 diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseListAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseListAdapter.kt index 6dc275c..8359767 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseListAdapter.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseListAdapter.kt @@ -12,7 +12,7 @@ import com.appttude.h_mal.farmr.utils.show abstract class BaseListAdapter( diff: DiffUtil.ItemCallback, private val layoutId: Int, - private val emptyView: View + private val emptyView: View? ) : ListAdapter(diff) { override fun onCreateViewHolder( @@ -49,8 +49,8 @@ abstract class BaseListAdapter( } fun checkEmpty() { - if (itemCount == 0) emptyView.show() - else emptyView.hide() + if (itemCount == 0) emptyView?.show() + else emptyView?.hide() } }) } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/ChildFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/ChildFragment.kt new file mode 100644 index 0000000..7fab86d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/ChildFragment.kt @@ -0,0 +1,73 @@ +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.lifecycle.ViewModelProvider +import androidx.navigation.NavDirections +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getGenericClassAt +import com.appttude.h_mal.farmr.utils.navigateTo +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 java.io.IOException + +@Suppress("EmptyMethod") +abstract class ChildFragment(@LayoutRes contentLayoutId: Int) : + Fragment(contentLayoutId), KodeinAware { + + override val kodein by kodein() + private val factory by instance() + + lateinit var viewModel: V + + private val parent by lazy { requireParentFragment().requireParentFragment() } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel = + ViewModelProvider(parent, factory)[getGenericClassAt(0).java] + 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() {} + + /** + * 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?) {} + + + fun navigateParent(navArg: Any) { + when(navArg) { + is Int -> (parent).navigateTo(navArg) + is NavDirections -> (parent).navigateTo(navArg) + else -> { throw IOException("${navArg::class} is not a valid navigation argment") } + } + } + + fun setTitle(title: String) { + (parent as BaseFragment<*>).setTitle(title) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/FormFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/FormFragment.kt new file mode 100644 index 0000000..253ceb5 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/FormFragment.kt @@ -0,0 +1,46 @@ +package com.appttude.h_mal.farmr.base + +import android.text.Editable +import android.text.TextWatcher +import android.view.ViewGroup +import android.widget.EditText +import androidx.annotation.LayoutRes +import androidx.core.view.children + + +open class FormFragment(@LayoutRes contentLayoutId: Int) : BaseFragment(contentLayoutId) { + private val initialFormData = mutableMapOf() + private val formData = mutableMapOf() + + fun applyFormListener(view: ViewGroup) { + view.children.forEach { + if (it is EditText) { + initialFormData[it.id] = it.text.trim().toString() + setDataInMap(it.id, it.text.trim().toString()) + it.addCustomTextWatch() + } else if (it is ViewGroup) { + applyFormListener(it) + } + } + } + + fun didFormChange(): Boolean { + return !(initialFormData.all { (k, v) -> + formData[k] == v + }) + } + + private fun EditText.addCustomTextWatch() { + addTextChangedListener(object : TextWatcher{ + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { } + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + setDataInMap(id, p0.toString()) + } + override fun afterTextChanged(p0: Editable?) { } + }) + } + + private fun setDataInMap(id: Int, text: String) { + formData[id] = text + } +} \ 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 index 9feb218..5c03e06 100644 --- 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 @@ -22,5 +22,5 @@ interface Repository { timeIn: String?, timeOut: String?, type: String? - ) + ): Boolean } \ 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 index 27d4929..5a3a5ed 100644 --- 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 @@ -91,8 +91,8 @@ class RepositoryImpl( timeIn: String?, timeOut: String?, type: String? - ) { - preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type) + ): Boolean { + return 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/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt index 11481c1..1e0cec7 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 @@ -44,13 +44,13 @@ class PreferenceProvider( timeIn: String?, timeOut: String?, type: String? - ) { - preference.edit() + ): Boolean { + return preference.edit() .putString(DESCRIPTION, description) .putString(DATE_IN, timeIn) .putString(DATE_OUT, timeOut) .putString(TYPE, type) - .apply() + .commit() } fun getFilteringDetails(): Map { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/CalendarFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/CalendarFragment.kt new file mode 100644 index 0000000..c60c3a9 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/CalendarFragment.kt @@ -0,0 +1,54 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.applandeo.materialcalendarview.CalendarView +import com.applandeo.materialcalendarview.EventDay +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.ChildFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.tryGet +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import java.util.Calendar + +class CalendarFragment : ChildFragment(R.layout.fragment_calendar) { + private lateinit var shiftListView: RecyclerView + private lateinit var calendarView: CalendarView + + private lateinit var mAdapter: ShiftListAdapter + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + shiftListView = view.findViewById(R.id.shifts_available_recycler) + calendarView = view.findViewById(R.id.calendarView) + + mAdapter = ShiftListAdapter(this, null, viewModel) + shiftListView.adapter = mAdapter + + calendarView.setOnDayClickListener { populateShiftListsForDay(it.calendar) } + } + + override fun onResume() { + super.onResume() + viewModel.refreshLiveData() + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is List<*>) { + val events: List? = viewModel.retrieveEvents() + calendarView.setEvents(events) + + tryGet { calendarView.firstSelectedDate }?.let { + populateShiftListsForDay(it) + } + } + } + + private fun populateShiftListsForDay(calendar: Calendar) { + val data: List? = viewModel.getShiftsOnTheDay(calendar) + mAdapter.submitList(data) + } + +} \ 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 400c5a4..1b596ee 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 @@ -13,6 +13,7 @@ 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.goBack import com.appttude.h_mal.farmr.utils.setDatePicker import com.appttude.h_mal.farmr.viewmodel.FilterViewModel @@ -33,7 +34,6 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filte 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) @@ -75,6 +75,16 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filte submit.setOnClickListener(this) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(false) + } + + override fun onResume() { + super.onResume() + setTitle(getString(R.string.title_activity_filter_data)) + } + override fun onItemSelected( parentView: AdapterView<*>?, selectedItemView: View?, @@ -100,6 +110,6 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filte override fun onSuccess(data: Any?) { super.onSuccess(data) - if (data is Success) popBackStack() + if (data is Success) goBack() } } \ 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 ffcb0f3..98a42f9 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 @@ -1,7 +1,9 @@ package com.appttude.h_mal.farmr.ui import android.os.Bundle +import android.util.Log import android.view.View +import android.view.ViewGroup import android.widget.Button import android.widget.EditText import android.widget.LinearLayout @@ -9,8 +11,10 @@ import android.widget.RadioButton import android.widget.RadioGroup import android.widget.ScrollView import android.widget.TextView +import androidx.activity.OnBackPressedCallback import androidx.core.widget.doAfterTextChanged import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.FormFragment import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.data.room.converters.DateConverter @@ -22,16 +26,18 @@ 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.goBack 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.SubmissionViewModel -class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), - RadioGroup.OnCheckedChangeListener, BackPressedListener { +class FragmentAddItem : FormFragment(R.layout.fragment_add_item), + RadioGroup.OnCheckedChangeListener { + + private lateinit var onBackPressed: OnBackPressedCallback private val dateConverter = DateConverter() private val timeConverter = TimeConverter() @@ -122,61 +128,87 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ setupViewAfterViewCreated() } - private fun setupViewAfterViewCreated() { - id = arguments?.getLong(ID) - wholeView.hide() - - 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(dateConverter.fromDate(date)) - - // Set types - mType = ShiftType.getEnumByType(type) - mDescription = description - mDate = dateConverter.fromDate(date) - mPayRate = payRate!! - - when (ShiftType.getEnumByType(type)) { - ShiftType.HOURLY -> { - mHourlyRadioButton.isChecked = true - mPieceRadioButton.isChecked = false - mTimeInEditText.setText(timeConverter.fromTime(timeIn)) - mTimeOutEditText.setText(timeConverter.fromTime(timeOut)) - mBreakEditText.setText(breakMins.toString()) - val durationText = "${duration.formatToTwoDpString()} Hours" - mDurationTextView.text = durationText - - // Set fields - mTimeIn = timeConverter.fromTime(timeIn) - mTimeOut = timeConverter.fromTime(timeOut) - mBreaks = breakMins - } - - ShiftType.PIECE -> { - mHourlyRadioButton.isChecked = false - mPieceRadioButton.isChecked = true - mUnitEditText.setText(units?.formatToTwoDpString()) - - // Set piece rate units - mUnits = units - } - } - mPayRateEditText.setText(payRate.formatAsCurrencyString()) - mTotalPayTextView.text = totalPay?.formatAsCurrencyString() - - calculateTotalPay() - } - - // Return title - getString(R.string.edit_item_title) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(false) + // This callback is only called when MyFragment is at least started + onBackPressed = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + onBackPressed() } + } + requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressed) + id = try { + FragmentAddItemArgs.fromBundle(requireArguments()).shiftId + } catch (e: Exception) { + Log.i("Nav Args", "Failed to retrieve args from navigation") + null + } + } + + override fun onResume() { + super.onResume() + val title = when (arguments?.containsKey(ID)) { + true -> getString(R.string.edit_item_title) else -> getString(R.string.add_item_title) } setTitle(title) + + onBackPressed.isEnabled = true + } + + override fun onPause() { + super.onPause() + onBackPressed.isEnabled = false + } + + private fun setupViewAfterViewCreated() { + wholeView.hide() + + // Since we are editing a shift lets load the shift data into the views + id?.let { viewModel.getCurrentShift(it) }?.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 + mPieceRadioButton.isChecked = false + mTimeInEditText.setText(timeIn) + mTimeOutEditText.setText(timeOut) + 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.formatAsCurrencyString()) + mTotalPayTextView.text = totalPay.formatAsCurrencyString() + + calculateTotalPay() + } + + applyFormListener(view = view as ViewGroup) } override fun onCheckedChanged(radioGroup: RadioGroup, id: Int) { @@ -269,6 +301,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ StringBuilder().append(mDuration).append(" hours").toString() mDuration!! * mPayRate } + ShiftType.PIECE -> { (mUnits ?: 0f) * mPayRate } @@ -277,27 +310,26 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ } } - override fun onBackPressed(): Boolean { - if (mRadioGroup.checkedRadioButtonId == -1) { - mActivity?.popBackStack() - } else { + fun onBackPressed() { + if (didFormChange()) { requireContext().createDialog( title = "Discard Changes?", message = "Are you sure you want to discard changes?", displayCancel = true, okCallback = { _, _ -> - mActivity?.popBackStack() + goBack() } ) + } else { + goBack() } - return true } override fun onSuccess(data: Any?) { super.onSuccess(data) if (data is Success) { displayToast(data.successMessage) - popBackStack() + goBack() } } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentList.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentList.kt new file mode 100644 index 0000000..f0afc55 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentList.kt @@ -0,0 +1,46 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.ChildFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.navigateTo +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.google.android.material.floatingactionbutton.FloatingActionButton + + +class FragmentList : ChildFragment(R.layout.fragment_list) { + private lateinit var productListView: RecyclerView + private lateinit var emptyView: View + private lateinit var mAdapter: ShiftListAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + emptyView = view.findViewById(R.id.empty_view) + productListView = view.findViewById(R.id.list_item_view) + + mAdapter = ShiftListAdapter(this, emptyView, viewModel) + productListView.adapter = mAdapter + } + + override fun onStart() { + super.onStart() + viewModel.refreshLiveData() + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is List<*>) { + @Suppress("UNCHECKED_CAST") + mAdapter.submitList(data as List) + } else if (data is Success) { + displayToast(data.successMessage) + } + } + +} \ 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 a8a7487..fe992f2 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 @@ -3,71 +3,85 @@ package com.appttude.h_mal.farmr.ui import android.app.AlertDialog import android.content.Intent import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.activity.OnBackPressedCallback import androidx.core.content.FileProvider -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController 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.utils.navigateTo import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.floatingactionbutton.FloatingActionButton import java.io.File 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 +class FragmentMain : BaseFragment(R.layout.fragment_main) { + private lateinit var onBackPressed: OnBackPressedCallback + + lateinit var navView: BottomNavigationView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setTitle("Shift List") // Inflate the layout for this fragment setHasOptionsMenu(true) + // This callback is only called when MyFragment is at least started + onBackPressed = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + onBackPressed() + } + } + requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressed) + } + + override fun onResume() { + super.onResume() + + onBackPressed.isEnabled = true + } + + override fun onPause() { + super.onPause() + onBackPressed.isEnabled = false + + viewModel.saveBottomBarState(navView.selectedItemId) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - emptyView = view.findViewById(R.id.empty_view) - productListView = view.findViewById(R.id.list_item_view) + navView = view.findViewById(R.id.bottom_bar) + val navHost = childFragmentManager.findFragmentById(R.id.sub_container) as NavHostFragment - mAdapter = ShiftListAdapter(this, emptyView) { - viewModel.deleteShift(it) + val navController = navHost.navController + navController.setGraph(R.navigation.home_navigation) + + navView.setupWithNavController(navController) + + viewModel.getBottomBarState()?.let { + navView.selectedItemId = it + } + + navController.addOnDestinationChangedListener { _, destination, _ -> + setTitle(destination.label.toString()) } - productListView.adapter = mAdapter view.findViewById(R.id.fab1).setOnClickListener { - navigateToFragment(FragmentAddItem(), name = "additem") + navigateTo(R.id.main_to_addItem) } } - override fun onStart() { - super.onStart() - viewModel.refreshLiveData() - } - - override fun onSuccess(data: Any?) { - super.onSuccess(data) - if (data is List<*>) { - @Suppress("UNCHECKED_CAST") - mAdapter.submitList(data as List) - } - if (data is Success) { - displayToast(data.successMessage) - } + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // Inflate the menu; this adds items to the action bar if it is present. + inflater.inflate(R.menu.menu_main, menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -87,7 +101,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } R.id.filter_data -> { - navigateToFragment(FilterDataFragment(), name = "filterdata") + navigateTo(R.id.main_to_filterData) return true } @@ -170,21 +184,13 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr file ) intent.setDataAndType(excelUri, "application/vnd.ms-excel") - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) startActivity(intent) } } - private fun exportDialog() { - AlertDialog.Builder(context) - .setTitle("Export?") - .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok) { _, _ -> exportData() }.create().show() - } - - override fun onBackPressed(): Boolean { + fun onBackPressed() { requireContext().createDialog( title = "Leave?", message = "Are you sure you want to exit Farmr?", @@ -198,6 +204,5 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr 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 index be79fa7..0f3bf1c 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,9 @@ 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.formatToTwoDp import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.navigateTo import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.viewmodel.InfoViewModel @@ -35,7 +37,6 @@ class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher 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) @@ -52,11 +53,24 @@ 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 = FurtherInfoFragmentArgs.fromBundle(requireArguments()).shiftId + editButton.setOnClickListener { - navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!) + val nav = FurtherInfoFragmentDirections.furtherInfoToAddItem(id) + navigateTo(nav) } - viewModel.retrieveData(arguments) + viewModel.retrieveData(id) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(false) + } + + override fun onResume() { + super.onResume() + setTitle(getString(R.string.further_info_title)) } override fun onSuccess(data: Any?) { @@ -91,7 +105,7 @@ class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher unitsTV.text = units.toString() val paymentSummary = - StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ") + StringBuilder().append(units.formatToTwoDp()).append(" Units @ ") .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n") .append("Equals: ").append(totalPay.formatAsCurrencyString()) totalPayTV.text = paymentSummary 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 0ae17b4..9f0357b 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,19 +1,15 @@ package com.appttude.h_mal.farmr.ui -import android.Manifest -import android.app.Activity -import android.content.pm.PackageManager import android.os.Bundle -import android.view.Menu +import android.view.MenuItem import androidx.appcompat.widget.Toolbar -import androidx.core.app.ActivityCompat +import androidx.navigation.fragment.NavHostFragment 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.popBackStack class MainActivity : BaseActivity() { private lateinit var toolbar: Toolbar + private lateinit var navHost: NavHostFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,26 +17,19 @@ class MainActivity : BaseActivity() { toolbar = findViewById(R.id.toolbar) setSupportActionBar(toolbar) - val fragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.container, FragmentMain()).addToBackStack("main").commit() + navHost = supportFragmentManager + .findFragmentById(R.id.container) as NavHostFragment + val navController = navHost.navController + navController.setGraph(R.navigation.shift_navigation) } - 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() - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + when (item.itemId) { + android.R.id.home -> onBackPressed() } + return super.onOptionsItemSelected(item) } } \ No newline at end of file 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 82d8d52..70723a4 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 @@ -2,24 +2,29 @@ package com.appttude.h_mal.farmr.ui import android.annotation.SuppressLint import android.app.AlertDialog -import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import androidx.fragment.app.Fragment +import androidx.appcompat.widget.PopupMenu import androidx.recyclerview.widget.DiffUtil import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BaseListAdapter +import com.appttude.h_mal.farmr.base.ChildFragment 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.formatToTwoDpString -import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.viewmodel.MainViewModel + + +const val PIECE_ITEM = 500 +const val HOURLY_ITEM = 501 class ShiftListAdapter( - private val fragment: Fragment, - emptyView: View, - private val longPressCallback: (Long) -> Unit + private val fragment: ChildFragment<*>, + emptyView: View?, + private val viewModel: MainViewModel ) : BaseListAdapter(diffCallBack, R.layout.list_item_1, emptyView) { @SuppressLint("SetTextI18n") @@ -30,70 +35,91 @@ class ShiftListAdapter( 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.formatToTwoDpString() - descriptionTextView.text = descriptionText - dateTextView.text = dateText - totalPay.text = totalPayText + when (getItemViewType(position)) { + HOURLY_ITEM -> { + val hoursView: TextView = view.findViewById(R.id.hours) + val minutesView: TextView = view.findViewById(R.id.minutes) - when (ShiftType.getEnumByType(typeText)) { - ShiftType.HOURLY -> { val time = data.getHoursMinutesPairFromDuration() - hoursView.text = time.first - minutesView.text = time.second + minutesView.text = if (time.second.length == 1) "0${time.second}" else time.second } - ShiftType.PIECE -> { + PIECE_ITEM -> { + val unitsView: TextView = view.findViewById(R.id.pieces) val unitsText: String = data.units.toString() - hoursView.text = unitsText - h.text = "" - minutesView.text = "" - m.text = "pcs" + unitsView.text = unitsText } } + descriptionTextView.text = data.description + dateTextView.text = data.date + totalPay.text = data.totalPay.formatToTwoDpString() - val b: Bundle = Bundle() - b.putLong(ID, data.id) view.setOnClickListener { // Navigate to further info - fragment.navigateToFragment( - FurtherInfoFragment(), - bundle = b, - name = "furtherinfo" - ) + val nav = FragmentMainDirections.mainToFurtherInfo(data.id) + fragment.navigateParent(nav) } 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") { _, _ -> longPressCallback.invoke(data.id) } - .setNegativeButton("cancel") { dialog, _ -> - dialog?.dismiss() + //creating a popup menu + val popup = PopupMenu(it.context, it) + //inflating menu from xml resource + popup.inflate(R.menu.options_menu) + + //adding click listener + popup.setOnMenuItemClickListener { menu -> + when (menu.itemId) { + R.id.update -> { + // Navigate to edit + val nav = FragmentMainDirections.mainToAddItem(data.id) + fragment.navigateParent(nav) + return@setOnMenuItemClickListener true + } + + R.id.delete -> { + AlertDialog.Builder(it.context) + .setMessage("Are you sure you want to delete") + .setPositiveButton("delete") { _, _ -> viewModel.deleteShift(data.id) } + .setNegativeButton("cancel") { dialog, _ -> + dialog?.dismiss() + } + .create().show() + return@setOnMenuItemClickListener true + } + + else -> return@setOnMenuItemClickListener false } - .create().show() - true + } + //displaying the popup + popup.show() } } + override fun getItemViewType(position: Int): Int { + val typeString = getItem(position).type + return when (ShiftType.getEnumByType(typeString)) { + ShiftType.HOURLY -> HOURLY_ITEM + ShiftType.PIECE -> PIECE_ITEM + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CurrentViewHolder { + val layoutId = when (viewType) { + HOURLY_ITEM -> R.layout.list_cell_hourly + PIECE_ITEM -> R.layout.list_cell_piece + else -> { + return super.onCreateViewHolder(parent, viewType) + } + } + val currentView = LayoutInflater + .from(parent.context) + .inflate(layoutId, parent, false) + return CurrentViewHolder(currentView) + } + companion object { val diffCallBack = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { 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 index 59f48f8..fc14614 100644 --- 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 @@ -4,4 +4,5 @@ const val LEGACY = "LEGACY_" const val DATE_FORMAT = "yyyy-MM-dd" const val TIME_FORMAT = "hh:mm" const val ID = "ID" +const val SHIFT_ID = "shiftId" 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 index e0f3582..f3453fe 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,6 +1,7 @@ package com.appttude.h_mal.farmr.utils import java.io.IOException +import java.math.RoundingMode import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar @@ -26,7 +27,7 @@ fun Float.formatAsCurrencyString(): String? { } fun Float.formatToTwoDpString(): String { - return toBigDecimal().setScale(2).toString() + return toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toString() } fun String.dateStringIsValid(): Boolean { @@ -47,6 +48,13 @@ fun String.convertDateString(format: String = DATE_FORMAT): Date? { return formatter.parse(this) } +fun String.convertToCalendar(format: String = DATE_FORMAT): Calendar? { + val date = convertDateString(format) + val calendar = Calendar.getInstance() + calendar.time = date ?: return null + return calendar +} + /** * turns "HH:mm" into an hour and minutes pair * 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 index cb0d32c..0dbcd89 100644 --- 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 @@ -20,7 +20,7 @@ fun Any.getGenericClassAt(position: Int): KClass = * var s: String? * i.validate{!i.isNullOrEmpty()} { print("string is empty") } */ -inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Unit) { +inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Unit) { if (!validate.invoke(this)) { onError.invoke() } @@ -30,9 +30,25 @@ inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Uni * 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 { +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)) } +} + +/** + * Tries to retrieve a variable that may throw an exception + * + * @Returns variable if successful else null + */ +inline fun tryGet(validate: () -> T?): T? { + return try { + validate.invoke() + } catch (e: Exception) { + null + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/NavigationUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/NavigationUtils.kt new file mode 100644 index 0000000..9accd0e --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/NavigationUtils.kt @@ -0,0 +1,33 @@ +package com.appttude.h_mal.farmr.utils + +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.NavDirections +import androidx.navigation.Navigation +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.ChildFragment + +fun Fragment.navigateToFragment(newFragment: Fragment) { + childFragmentManager.beginTransaction() + .add(R.id.container, newFragment) + .commit() +} + + +fun View.navigateTo(navigationId: Int) { + Navigation.findNavController(this).navigate(navigationId) +} + +fun View.navigateTo(navDirections: NavDirections) { + Navigation.findNavController(this).navigate(navDirections) +} + +fun Fragment.navigateTo(navigationId: Int) { + Navigation.findNavController(requireView()).navigate(navigationId) +} + +fun Fragment.navigateTo(navDirections: NavDirections) { + Navigation.findNavController(requireView()).navigate(navDirections) +} + +fun Fragment.goBack() = Navigation.findNavController(requireView()).popBackStack() \ 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 index 025829e..8692912 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 @@ -10,13 +10,7 @@ class InfoViewModel( repository: Repository ) : ShiftViewModel(repository) { - fun retrieveData(bundle: Bundle?) { - val id = bundle?.getLong(ID) - if (id == null) { - onError("Failed to retrieve shift") - return - } - + fun retrieveData(id: Long) { val shift = getCurrentShift(id) if (shift == null) { onError("Failed to retrieve shift") 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 1919ed1..d6feb30 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,7 +1,11 @@ package com.appttude.h_mal.farmr.viewmodel +import android.graphics.Color +import android.graphics.drawable.Drawable import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.applandeo.materialcalendarview.EventDay +import com.appttude.h_mal.farmr.R 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,6 +25,7 @@ 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.convertDateString +import com.appttude.h_mal.farmr.utils.convertToCalendar import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.sortedByOrder import jxl.Workbook @@ -30,6 +35,7 @@ import jxl.write.WritableWorkbook import jxl.write.WriteException import java.io.File import java.io.IOException +import java.util.Calendar import java.util.Locale @@ -42,8 +48,17 @@ class MainViewModel( private var mSort: Sortable = Sortable.ID private var mOrder: Order = Order.ASCENDING + private var selectedItemId: Int? = null + private val observer = Observer> { it?.let { updateFiltrationAndPostResults(it) } + private var selectedItemId: Int? = null + + private val observer = Observer> { + it?.let { + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) + } } init { @@ -103,7 +118,7 @@ class MainViewModel( if (second == null) return compareDate.after(first) if (first == null) return compareDate.before(second) - return compareDate.after(first) && compareDate.before(second) + return compareDate.compareTo(first) * second.compareTo(compareDate) >= 0 } /* @@ -211,9 +226,8 @@ class MainViewModel( } fun clearFilters() { - super.setFiltrationDetails(null, null, null, null) - onSuccess(Success("Filters have been cleared")) - refreshLiveData() + val result = super.setFiltrationDetails(null, null, null, null) + if (result) refreshLiveData() } /* @@ -298,4 +312,25 @@ class MainViewModel( return null } + fun retrieveEvents(): List? { + val shiftList = shiftLiveData.value ?: return null + return shiftList.applyFilters().mapNotNull { + it.date.convertToCalendar() + ?.let { d -> EventDay(d, R.drawable.baseline_list_alt_24, Color.parseColor("#228B22")) } + } + } + + fun getShiftsOnTheDay(calendar: Calendar): List? { + val shiftList = shiftLiveData.value ?: return null + return shiftList.filter { it.date.convertToCalendar()?.compareTo(calendar) == 0 } + } + + fun saveBottomBarState(selectedItemId: Int) { + this.selectedItemId = selectedItemId + } + + fun getBottomBarState(): Int? { + return selectedItemId + } + } \ No newline at end of file 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 abcc710..a4761d7 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 @@ -18,25 +18,13 @@ open class ShiftViewModel( */ 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) + ): Boolean { + return repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) } open fun getFiltrationDetails(): FilterStore { 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 index b72fa46..5244ddf 100644 --- 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 @@ -289,6 +289,7 @@ class SubmissionViewModel( description = description, date = date, units = units!!, + rateOfPay = rateOfPay, ) } diff --git a/app/src/main/res/drawable/baseline_calendar_month_24.xml b/app/src/main/res/drawable/baseline_calendar_month_24.xml new file mode 100644 index 0000000..b89360f --- /dev/null +++ b/app/src/main/res/drawable/baseline_calendar_month_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_list_alt_24.xml b/app/src/main/res/drawable/baseline_list_alt_24.xml new file mode 100644 index 0000000..57fd027 --- /dev/null +++ b/app/src/main/res/drawable/baseline_list_alt_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_more_vert_24.xml b/app/src/main/res/drawable/baseline_more_vert_24.xml new file mode 100644 index 0000000..39fbab5 --- /dev/null +++ b/app/src/main/res/drawable/baseline_more_vert_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-v26/list_cell_hourly.xml b/app/src/main/res/layout-v26/list_cell_hourly.xml new file mode 100644 index 0000000..ef855a8 --- /dev/null +++ b/app/src/main/res/layout-v26/list_cell_hourly.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-v26/list_cell_piece.xml b/app/src/main/res/layout-v26/list_cell_piece.xml new file mode 100644 index 0000000..89d5ebd --- /dev/null +++ b/app/src/main/res/layout-v26/list_cell_piece.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_calendar.xml b/app/src/main/res/layout/fragment_calendar.xml new file mode 100644 index 0000000..ae7485a --- /dev/null +++ b/app/src/main/res/layout/fragment_calendar.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list.xml b/app/src/main/res/layout/fragment_list.xml new file mode 100644 index 0000000..dbb12ec --- /dev/null +++ b/app/src/main/res/layout/fragment_list.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index ea05026..bd040fa 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -1,34 +1,39 @@ - - - + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/bottom_bar" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:layout="@layout/fragment_calendar" /> + + - - - + diff --git a/app/src/main/res/layout/list_cell_hourly.xml b/app/src/main/res/layout/list_cell_hourly.xml new file mode 100644 index 0000000..46f178a --- /dev/null +++ b/app/src/main/res/layout/list_cell_hourly.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_cell_piece.xml b/app/src/main/res/layout/list_cell_piece.xml new file mode 100644 index 0000000..1e7c87c --- /dev/null +++ b/app/src/main/res/layout/list_cell_piece.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_1.xml b/app/src/main/res/layout/list_item_1.xml index bdec9ea..672d4e9 100644 --- a/app/src/main/res/layout/list_item_1.xml +++ b/app/src/main/res/layout/list_item_1.xml @@ -1,6 +1,7 @@ + android:showDividers="end" + tools:ignore="HardcodedText"> + android:textSize="@dimen/unit_text_size" /> + android:textSize="@dimen/units_symbol_size" /> + android:textSize="@dimen/unit_text_size"/> + android:textSize="@dimen/units_symbol_size" /> + android:textSize="@dimen/units_symbol_size" /> + android:textSize="@dimen/total_pay_size" /> @@ -106,7 +108,7 @@ android:layout_below="@+id/date" android:maxLines="3" android:text="Location Name" - android:textSize="20sp" /> + android:textSize="@dimen/location_size" /> + android:textSize="@dimen/date_size" /> - - + app:defaultNavHost="true" + android:id="@+id/container" + tools:layout="@layout/fragment_main"> + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_navigation_menu.xml b/app/src/main/res/menu/bottom_navigation_menu.xml new file mode 100644 index 0000000..4ce7d48 --- /dev/null +++ b/app/src/main/res/menu/bottom_navigation_menu.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/options_menu.xml b/app/src/main/res/menu/options_menu.xml new file mode 100644 index 0000000..103bea7 --- /dev/null +++ b/app/src/main/res/menu/options_menu.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/home_navigation.xml b/app/src/main/res/navigation/home_navigation.xml new file mode 100644 index 0000000..51e09ab --- /dev/null +++ b/app/src/main/res/navigation/home_navigation.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/shift_navigation.xml b/app/src/main/res/navigation/shift_navigation.xml new file mode 100644 index 0000000..2ceec49 --- /dev/null +++ b/app/src/main/res/navigation/shift_navigation.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 74f8724..53a1571 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,6 +4,13 @@ 16dp 16dp 8dp + 20sp + 12sp + 16sp + 16sp + 14sp + 75dp + 12sp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7120641..483c7b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,14 +87,18 @@ Export Data Sort - ca-app-pub-3406791512187471/7557456476 - ca-app-pub-3406791512187471~9541579845 - Help & Support - - Hello blank fragment Shift Details insert break in minutes Break + h + m + pcs + £ + Delete Shift + Update Shift + Floating action button + Shifts + Calendar 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 index 3a537cb..23ba6eb 100644 --- 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 @@ -56,12 +56,10 @@ class InfoViewModelTest : ShiftViewModelTest() { 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) + viewModel.retrieveData(id) // Assert assertEquals( 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 1a3e6ad..38c5b1b 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 @@ -112,7 +112,7 @@ class MainViewModelTest { val retrievedShifts = retrieveCurrentData() val description = viewModel.getInformation() - every { repository.setFilteringDetailsInPrefs(null, null, null, null) }.returns(Unit) + every { repository.setFilteringDetailsInPrefs(null, null, null, null) }.returns(true) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) viewModel.clearFilters() val descriptionAfterClearedFilter = viewModel.getInformation() diff --git a/build.gradle b/build.gradle index 4faecfe..12d88d4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } } dependencies { + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$NAVIGATION_VERSION" classpath "com.android.tools.build:gradle:$GRADLE_PLUGIN_VERSION" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath "com.autonomousapps:dependency-analysis-gradle-plugin:$GRADLE_ANALYZE_VERSION" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5d272f7..dc10e67 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,7 +23,7 @@ platform :android do desc "Deploy a new version to the Google Play" lane :deploy do - gradle(task: "clean assembleRelease") + gradle(task: "clean bundle", build_type: "Release") upload_to_play_store end end diff --git a/gradle.properties b/gradle.properties index ae744b0..30ff04f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,7 @@ MATERIAL_VERSION = 1.0.0 CONSTR_LAYOUT_VERSION = 1.1.3 LIFECYCLE_VERSION = 2.5.1 VIEWMODEL_VERSION = 2.4.1 +NAVIGATION_VERSION = 2.3.2 PREFERENCES_VERSION = 1.2.1 MOKITO_INLINE_VERSION = 2.13.0 CORE_TEST_VERSION = 2.1.0 @@ -26,8 +27,8 @@ KOTLIN_VERSION = 1.7.10 GRADLE_ANALYZE_VERSION = 1.20.0 # Android configuration -COMPILE_SDK_VERSION = android-31 -TARGET_SDK_VERSION = 31 +COMPILE_SDK_VERSION = android-33 +TARGET_SDK_VERSION = 33 MIN_SDK_VERSION = 21 # Gradle parameters