Merge branch 'master' into room_database_migration

# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt
#	app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt
#	app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt
This commit is contained in:
2023-09-15 17:43:21 +01:00
60 changed files with 1759 additions and 323 deletions

View File

@@ -39,7 +39,7 @@ commands:
- android/start-emulator-and-run-tests: - android/start-emulator-and-run-tests:
post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest
test-command: ./gradlew connectedDebugAndroidTest --continue 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 # store screenshots for failed ui tests
- when: - when:
condition: on_fail condition: on_fail
@@ -120,6 +120,7 @@ workflows:
only: only:
- master - master
- release - release
- /ui_test.*/
- deploy-to-playstore: - deploy-to-playstore:
context: appttude context: appttude
filters: filters:
@@ -127,4 +128,4 @@ workflows:
only: only:
- release - release
requires: requires:
- build-and-test - run_instrumentation_test

1
.gitignore vendored
View File

@@ -89,6 +89,7 @@ gen-external-apklibs
.idea/assetWizardSettings.xml .idea/assetWizardSettings.xml
.idea/gradle.xml .idea/gradle.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/navEditor.xml
# Gem/fastlane # Gem/fastlane
Gemfile.lock Gemfile.lock

View File

@@ -1,5 +1,6 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
@@ -14,8 +15,8 @@ android {
applicationId "com.appttude.h_mal.farmr" applicationId "com.appttude.h_mal.farmr"
minSdkVersion MIN_SDK_VERSION minSdkVersion MIN_SDK_VERSION
targetSdkVersion TARGET_SDK_VERSION targetSdkVersion TARGET_SDK_VERSION
versionCode 3 versionCode 7
versionName "2.1" versionName "3.0"
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
javaCompileOptions { javaCompileOptions {
@@ -60,6 +61,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-core:$LIFECYCLE_VERSION" implementation "androidx.lifecycle:lifecycle-livedata-core:$LIFECYCLE_VERSION"
implementation "androidx.lifecycle:lifecycle-viewmodel:$LIFECYCLE_VERSION" implementation "androidx.lifecycle:lifecycle-viewmodel:$LIFECYCLE_VERSION"
implementation 'androidx.recyclerview:recyclerview:1.0.0' 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 * / / * Unit testing * /
testImplementation "junit:junit:$JUNIT_VERSION" testImplementation "junit:junit:$JUNIT_VERSION"
androidTestRuntimeOnly "org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_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" implementation "org.kodein.di:kodein-di-framework-android-core:$KODEIN_VERSION"
/ * jxl * / / * jxl * /
implementation "net.sourceforge.jexcelapi:jxl:$JEXCEL_VERSION" implementation "net.sourceforge.jexcelapi:jxl:$JEXCEL_VERSION"
/ * calendar view * /
implementation 'com.applandeo:material-calendar-view:1.7.0'
} }

View File

@@ -15,6 +15,8 @@ import androidx.test.espresso.matcher.RootMatchers.withDecorView
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.application.TestAppClass 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 com.appttude.h_mal.farmr.ui.utils.getShifts
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.hamcrest.Matcher import org.hamcrest.Matcher
@@ -73,16 +75,6 @@ open class BaseTest<A : Activity>(
open fun afterLaunch() {} open fun afterLaunch() {}
open fun testFinished() {} open fun testFinished() {}
fun waitFor(delay: Long) {
Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> = ViewMatchers.isRoot()
override fun getDescription(): String = "wait for $delay milliseconds"
override fun perform(uiController: UiController, v: View?) {
uiController.loopMainThreadForAtLeast(delay)
}
})
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun checkToastMessage(message: String) { fun checkToastMessage(message: String) {
Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView))) Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView)))
@@ -95,7 +87,7 @@ open class BaseTest<A : Activity>(
fun navigateBack() = Espresso.pressBack() fun navigateBack() = Espresso.pressBack()
fun addRandomShifts() { fun addRandomShifts() {
testApp.addShiftsToDatabase(getShifts()) testApp.addShiftsToDatabase(generateShifts())
} }
fun clearDataBase() = testApp.clearDatabase() fun clearDataBase() = testApp.clearDatabase()

View File

@@ -23,6 +23,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anything import org.hamcrest.CoreMatchers.anything
@@ -58,6 +59,8 @@ open class BaseTestRobot {
fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) 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) { fun clickListItem(listRes: Int, position: Int) {
onData(anything()) onData(anything())
.inAdapterView(allOf(withId(listRes))) .inAdapterView(allOf(withId(listRes)))

View File

@@ -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<CurrentViewHolder>(R.id.shifts_available_recycler, text)
fun clickOnListItemAtPosition(position: Int) =
clickRecyclerAtPosition<CurrentViewHolder>(R.id.shifts_available_recycler, position)
fun clickOnEditForItem(position: Int) {
clickViewInRecyclerAtPosition<CurrentViewHolder>(
R.id.shifts_available_recycler,
position,
R.id.imageView
)
onView(withId(R.id.update)).perform(click())
}
fun clickOnDeleteForItem(position: Int) {
clickViewInRecyclerAtPosition<CurrentViewHolder>(
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)
}

View File

@@ -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)
}
}

View File

@@ -1,7 +1,9 @@
package com.appttude.h_mal.farmr.ui.robots 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.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.Order
import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Sortable
import com.appttude.h_mal.farmr.ui.BaseTestRobot 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() } fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
class HomeScreenRobot : BaseTestRobot() { class HomeScreenRobot : BaseTestRobot() {
fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText<CurrentViewHolder>(R.id.list_item_view, text)
fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position, R.id.imageView)
fun clickFab() = clickButton(R.id.fab1) fun clickFab() = clickButton(R.id.fab1)
fun clickOnInfoIcon() = clickButton(R.id.action_favorite) fun clickOnInfoIcon() = clickButton(R.id.action_favorite)
fun clickFilterInMenu() = clickOnMenuItem(R.string.filter) fun clickFilterInMenu() = clickOnMenuItem(R.string.filter)
@@ -25,4 +24,16 @@ class HomeScreenRobot : BaseTestRobot() {
val orderLabel = order.label val orderLabel = order.label
clickDialogButton(orderLabel) 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
}
} }

View File

@@ -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<CurrentViewHolder>(R.id.list_item_view, text)
fun clickOnItemAtPosition(position: Int) =
clickRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
fun clickOnEditForItem(position: Int) {
clickViewInRecyclerAtPosition<CurrentViewHolder>(
R.id.list_item_view,
position,
R.id.imageView
)
waitFor(800)
EspressoHelper.waitForView(withText("Update Shift")).perform(click())
}
fun clickOnDeleteForItem(position: Int) {
clickViewInRecyclerAtPosition<CurrentViewHolder>(
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)))
}

View File

@@ -23,9 +23,9 @@ class ViewItemScreenRobot : BaseTestRobot() {
matchText(R.id.details_time, "$timeIn-$timeOut") matchText(R.id.details_time, "$timeIn-$timeOut")
} }
fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, mins.toString()) fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, "$mins mins")
fun matchUnits(units: Float) = fillEditText(R.id.details_units, units.toString()) fun matchUnits(units: Float) = matchText(R.id.details_units, units.toString())
fun matchRateOfPay(rateOfPay: Float) = fillEditText(R.id.details_pay_rate, rateOfPay.toString()) fun matchRateOfPay(rateOfPay: Float) = matchText(R.id.details_pay_rate, rateOfPay.toString())
fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay) fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay)
fun matchDuration(duration: String) = matchText(R.id.details_duration, duration) fun matchDuration(duration: String) = matchText(R.id.details_duration, duration)

View File

@@ -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>(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()
}
}
}

View File

@@ -1,15 +1,25 @@
package com.appttude.h_mal.farmr.ui.tests 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.Order
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Sortable
import com.appttude.h_mal.farmr.ui.BaseTest import com.appttude.h_mal.farmr.ui.BaseTest
import com.appttude.h_mal.farmr.ui.MainActivity 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.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.filterScreen
import com.appttude.h_mal.farmr.ui.robots.homeScreen 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.robots.viewScreen
import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitFor
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.util.Calendar
import java.util.Calendar.MONTH
import java.util.Calendar.YEAR
class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) { class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
@@ -33,7 +43,7 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
// Add a shift successfully // Add a shift successfully
@Test @Test
fun openAddScreen_addNewShift_newShiftCreated() { fun openAddScreen_addNewHourlyShift_assertShiftDetail() {
homeScreen { homeScreen {
clickFab() clickFab()
} }
@@ -49,16 +59,25 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
assertTotalPay("£20.00") assertTotalPay("£20.00")
submit() submit()
} }
homeScreen { listScreen {
clickOnItemWithText("This is a description") 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 @Test
fun test2() { fun editShift_newDetailsAdded_assertShiftDetail() {
homeScreen { listScreen {
clickOnEdit(0) clickOnEditForItem(0)
} }
addScreen { addScreen {
setDescription("Edited this shift") setDescription("Edited this shift")
@@ -70,7 +89,7 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
assertTotalPay("£40.00") assertTotalPay("£40.00")
submit() submit()
} }
homeScreen { listScreen {
clickOnItemWithText("Edited this shift") clickOnItemWithText("Edited this shift")
} }
viewScreen { viewScreen {
@@ -80,12 +99,13 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
} }
} }
// filter the list with date from
@Test @Test
fun test3() { fun applySort_listIsSorted_assertShiftsSortedCorrectly() {
homeScreen { homeScreen {
applySort(Sortable.TYPE, Order.DESCENDING) applySort(Sortable.TYPE, Order.DESCENDING)
clickOnItemAtPosition(0) listScreen {
clickOnItemAtPosition(0)
}
viewScreen { viewScreen {
matchDescription("Day five") matchDescription("Day five")
matchShiftType(ShiftType.PIECE) matchShiftType(ShiftType.PIECE)
@@ -93,25 +113,26 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
} }
} }
// filter the list with date to
@Test @Test
fun test4() { fun applyDateBetweenFilterAndClear_listIsFilteredByDate_assertFilteredResultsCorrectly() {
homeScreen { homeScreen {
clickFilterInMenu() clickFilterInMenu()
} }
filterScreen { filterScreen {
setDateIn(2023,8,3) val calendar = Calendar.getInstance()
setDateOut(2023,8,6) val year = calendar.get(YEAR)
val month = calendar.get(MONTH) + 1
setDateIn(year, month, 3)
setDateOut(year, month, 6)
submit() submit()
} }
homeScreen { listScreen {
clickOnItemAtPosition(0) assertListCount(4)
} }
} }
// Add a shift as piece rate
@Test @Test
fun test5() { fun openAddScreen_addNewPieceShift_assertShiftDetail() {
homeScreen { homeScreen {
clickFab() clickFab()
} }
@@ -124,18 +145,44 @@ class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
assertTotalPay("£10.00") assertTotalPay("£10.00")
submit() submit()
} }
homeScreen { listScreen {
clickOnItemWithText("This is a description") 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 @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 @Test
fun test7() { fun deleteShift_confirmDelete_assertShiftDeleted() {
listScreen {
clickOnDeleteForItem(0)
confirmDeleteItemOnDialog()
clickOnItemAtPosition(0)
}
viewScreen {
matchDescription("Day two")
}
} }
} }

View File

@@ -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.Shift
import com.appttude.h_mal.farmr.model.ShiftType 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( fun getShifts() = listOf(
Shift( Shift(
@@ -100,4 +107,113 @@ fun getShifts() = listOf(
10f, 10f,
10f 10f
) )
) )
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<Shift> {
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
)
)
}

View File

@@ -4,11 +4,13 @@ import android.os.SystemClock.sleep
import android.view.View import android.view.View
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.Checkable import android.widget.Checkable
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.util.TreeIterables import androidx.test.espresso.util.TreeIterables
import org.hamcrest.BaseMatcher import org.hamcrest.BaseMatcher
@@ -120,4 +122,14 @@ object EspressoHelper {
throw Exception("Error finding a view matching $viewMatcher") throw Exception("Error finding a view matching $viewMatcher")
} }
fun waitFor(delay: Long) {
onView(isRoot()).perform(object : ViewAction {
override fun getConstraints(): Matcher<View> = isRoot()
override fun getDescription(): String = "wait for $delay milliseconds"
override fun perform(uiController: UiController, v: View?) {
uiController.loopMainThreadForAtLeast(delay)
}
})
}
} }

View File

@@ -29,13 +29,6 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
var mActivity: BaseActivity? = null var mActivity: BaseActivity? = null
private var shortAnimationDuration by Delegates.notNull<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mActivity = requireActivity() as BaseActivity mActivity = requireActivity() as BaseActivity

View File

@@ -12,7 +12,7 @@ import com.appttude.h_mal.farmr.utils.show
abstract class BaseListAdapter<T : Any>( abstract class BaseListAdapter<T : Any>(
diff: DiffUtil.ItemCallback<T>, diff: DiffUtil.ItemCallback<T>,
private val layoutId: Int, private val layoutId: Int,
private val emptyView: View private val emptyView: View?
) : ListAdapter<T, BaseListAdapter.CurrentViewHolder>(diff) { ) : ListAdapter<T, BaseListAdapter.CurrentViewHolder>(diff) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
@@ -49,8 +49,8 @@ abstract class BaseListAdapter<T : Any>(
} }
fun checkEmpty() { fun checkEmpty() {
if (itemCount == 0) emptyView.show() if (itemCount == 0) emptyView?.show()
else emptyView.hide() else emptyView?.hide()
} }
}) })
} }

View File

@@ -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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int) :
Fragment(contentLayoutId), KodeinAware {
override val kodein by kodein()
private val factory by instance<ApplicationViewModelFactory>()
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<V>(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)
}
}

View File

@@ -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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int) : BaseFragment<V>(contentLayoutId) {
private val initialFormData = mutableMapOf<Int, String>()
private val formData = mutableMapOf<Int, String>()
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
}
}

View File

@@ -22,5 +22,5 @@ interface Repository {
timeIn: String?, timeIn: String?,
timeOut: String?, timeOut: String?,
type: String? type: String?
) ): Boolean
} }

View File

@@ -91,8 +91,8 @@ class RepositoryImpl(
timeIn: String?, timeIn: String?,
timeOut: String?, timeOut: String?,
type: String? type: String?
) { ): Boolean {
preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type) return preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type)
} }
} }

View File

@@ -44,13 +44,13 @@ class PreferenceProvider(
timeIn: String?, timeIn: String?,
timeOut: String?, timeOut: String?,
type: String? type: String?
) { ): Boolean {
preference.edit() return preference.edit()
.putString(DESCRIPTION, description) .putString(DESCRIPTION, description)
.putString(DATE_IN, timeIn) .putString(DATE_IN, timeIn)
.putString(DATE_OUT, timeOut) .putString(DATE_OUT, timeOut)
.putString(TYPE, type) .putString(TYPE, type)
.apply() .commit()
} }
fun getFilteringDetails(): Map<String, String?> { fun getFilteringDetails(): Map<String, String?> {

View File

@@ -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<MainViewModel>(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<EventDay>? = viewModel.retrieveEvents()
calendarView.setEvents(events)
tryGet { calendarView.firstSelectedDate }?.let {
populateShiftListsForDay(it)
}
}
}
private fun populateShiftListsForDay(calendar: Calendar) {
val data: List<ShiftObject>? = viewModel.getShiftsOnTheDay(calendar)
mAdapter.submitList(data)
}
}

View File

@@ -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.base.BaseFragment
import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success 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.utils.setDatePicker
import com.appttude.h_mal.farmr.viewmodel.FilterViewModel import com.appttude.h_mal.farmr.viewmodel.FilterViewModel
@@ -33,7 +34,6 @@ class FilterDataFragment : BaseFragment<FilterViewModel>(R.layout.fragment_filte
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setTitle(getString(R.string.title_activity_filter_data))
LocationET = view.findViewById(R.id.filterLocationEditText) LocationET = view.findViewById(R.id.filterLocationEditText)
dateFromET = view.findViewById(R.id.fromdateInEditText) dateFromET = view.findViewById(R.id.fromdateInEditText)
@@ -75,6 +75,16 @@ class FilterDataFragment : BaseFragment<FilterViewModel>(R.layout.fragment_filte
submit.setOnClickListener(this) 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( override fun onItemSelected(
parentView: AdapterView<*>?, parentView: AdapterView<*>?,
selectedItemView: View?, selectedItemView: View?,
@@ -100,6 +110,6 @@ class FilterDataFragment : BaseFragment<FilterViewModel>(R.layout.fragment_filte
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
if (data is Success) popBackStack() if (data is Success) goBack()
} }
} }

View File

@@ -1,7 +1,9 @@
package com.appttude.h_mal.farmr.ui package com.appttude.h_mal.farmr.ui
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -9,8 +11,10 @@ import android.widget.RadioButton
import android.widget.RadioGroup import android.widget.RadioGroup
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import com.appttude.h_mal.farmr.R 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.BackPressedListener
import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.base.BaseFragment
import com.appttude.h_mal.farmr.data.room.converters.DateConverter 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.displayToast
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
import com.appttude.h_mal.farmr.utils.formatToTwoDpString 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.hide
import com.appttude.h_mal.farmr.utils.popBackStack
import com.appttude.h_mal.farmr.utils.setDatePicker import com.appttude.h_mal.farmr.utils.setDatePicker
import com.appttude.h_mal.farmr.utils.setTimePicker import com.appttude.h_mal.farmr.utils.setTimePicker
import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.utils.show
import com.appttude.h_mal.farmr.utils.validateField import com.appttude.h_mal.farmr.utils.validateField
import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item), class FragmentAddItem : FormFragment<SubmissionViewModel>(R.layout.fragment_add_item),
RadioGroup.OnCheckedChangeListener, BackPressedListener { RadioGroup.OnCheckedChangeListener {
private lateinit var onBackPressed: OnBackPressedCallback
private val dateConverter = DateConverter() private val dateConverter = DateConverter()
private val timeConverter = TimeConverter() private val timeConverter = TimeConverter()
@@ -122,61 +128,87 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
setupViewAfterViewCreated() setupViewAfterViewCreated()
} }
private fun setupViewAfterViewCreated() { override fun onCreate(savedInstanceState: Bundle?) {
id = arguments?.getLong(ID) super.onCreate(savedInstanceState)
wholeView.hide() setHasOptionsMenu(false)
// This callback is only called when MyFragment is at least started
val title = when (arguments?.containsKey(ID)) { onBackPressed = object : OnBackPressedCallback(false) {
true -> { override fun handleOnBackPressed() {
// Since we are editing a shift lets load the shift data into the views onBackPressed()
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)
} }
}
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) else -> getString(R.string.add_item_title)
} }
setTitle(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) { override fun onCheckedChanged(radioGroup: RadioGroup, id: Int) {
@@ -269,6 +301,7 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
StringBuilder().append(mDuration).append(" hours").toString() StringBuilder().append(mDuration).append(" hours").toString()
mDuration!! * mPayRate mDuration!! * mPayRate
} }
ShiftType.PIECE -> { ShiftType.PIECE -> {
(mUnits ?: 0f) * mPayRate (mUnits ?: 0f) * mPayRate
} }
@@ -277,27 +310,26 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
} }
} }
override fun onBackPressed(): Boolean { fun onBackPressed() {
if (mRadioGroup.checkedRadioButtonId == -1) { if (didFormChange()) {
mActivity?.popBackStack()
} else {
requireContext().createDialog( requireContext().createDialog(
title = "Discard Changes?", title = "Discard Changes?",
message = "Are you sure you want to discard changes?", message = "Are you sure you want to discard changes?",
displayCancel = true, displayCancel = true,
okCallback = { _, _ -> okCallback = { _, _ ->
mActivity?.popBackStack() goBack()
} }
) )
} else {
goBack()
} }
return true
} }
override fun onSuccess(data: Any?) { override fun onSuccess(data: Any?) {
super.onSuccess(data) super.onSuccess(data)
if (data is Success) { if (data is Success) {
displayToast(data.successMessage) displayToast(data.successMessage)
popBackStack() goBack()
} }
} }
} }

View File

@@ -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<MainViewModel>(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<ShiftObject>)
} else if (data is Success) {
displayToast(data.successMessage)
}
}
}

View File

@@ -3,71 +3,85 @@ package com.appttude.h_mal.farmr.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.recyclerview.widget.RecyclerView import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.farmr.R 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.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.Order
import com.appttude.h_mal.farmr.model.Sortable 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.createDialog
import com.appttude.h_mal.farmr.utils.displayToast import com.appttude.h_mal.farmr.utils.navigateTo
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.MainViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPressedListener { class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main) {
private lateinit var productListView: RecyclerView private lateinit var onBackPressed: OnBackPressedCallback
private lateinit var emptyView: View
private lateinit var mAdapter: ShiftListAdapter lateinit var navView: BottomNavigationView
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setTitle("Shift List")
// Inflate the layout for this fragment // Inflate the layout for this fragment
setHasOptionsMenu(true) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
emptyView = view.findViewById(R.id.empty_view) navView = view.findViewById(R.id.bottom_bar)
productListView = view.findViewById(R.id.list_item_view) val navHost = childFragmentManager.findFragmentById(R.id.sub_container) as NavHostFragment
mAdapter = ShiftListAdapter(this, emptyView) { val navController = navHost.navController
viewModel.deleteShift(it) 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<FloatingActionButton>(R.id.fab1).setOnClickListener { view.findViewById<FloatingActionButton>(R.id.fab1).setOnClickListener {
navigateToFragment(FragmentAddItem(), name = "additem") navigateTo(R.id.main_to_addItem)
} }
} }
override fun onStart() { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onStart() // Inflate the menu; this adds items to the action bar if it is present.
viewModel.refreshLiveData() inflater.inflate(R.menu.menu_main, menu)
}
override fun onSuccess(data: Any?) {
super.onSuccess(data)
if (data is List<*>) {
@Suppress("UNCHECKED_CAST")
mAdapter.submitList(data as List<ShiftObject>)
}
if (data is Success) {
displayToast(data.successMessage)
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -87,7 +101,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
} }
R.id.filter_data -> { R.id.filter_data -> {
navigateToFragment(FilterDataFragment(), name = "filterdata") navigateTo(R.id.main_to_filterData)
return true return true
} }
@@ -170,21 +184,13 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
file file
) )
intent.setDataAndType(excelUri, "application/vnd.ms-excel") 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) startActivity(intent)
} }
} }
private fun exportDialog() { fun onBackPressed() {
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 {
requireContext().createDialog( requireContext().createDialog(
title = "Leave?", title = "Leave?",
message = "Are you sure you want to exit Farmr?", message = "Are you sure you want to exit Farmr?",
@@ -198,6 +204,5 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
exitProcess(0) exitProcess(0)
} }
) )
return true
} }
} }

View File

@@ -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.model.ShiftType
import com.appttude.h_mal.farmr.utils.CURRENCY import com.appttude.h_mal.farmr.utils.CURRENCY
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString 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.hide
import com.appttude.h_mal.farmr.utils.navigateTo
import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.utils.navigateToFragment
import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.utils.show
import com.appttude.h_mal.farmr.viewmodel.InfoViewModel import com.appttude.h_mal.farmr.viewmodel.InfoViewModel
@@ -35,7 +37,6 @@ class FurtherInfoFragment : BaseFragment<InfoViewModel>(R.layout.fragment_futher
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setTitle(getString(R.string.further_info_title))
progressBarFI = view.findViewById(R.id.progressBar_info) progressBarFI = view.findViewById(R.id.progressBar_info)
wholeView = view.findViewById(R.id.further_info_view) wholeView = view.findViewById(R.id.further_info_view)
@@ -52,11 +53,24 @@ class FurtherInfoFragment : BaseFragment<InfoViewModel>(R.layout.fragment_futher
hourlyDetailHolder = view.findViewById(R.id.details_hourly_details) hourlyDetailHolder = view.findViewById(R.id.details_hourly_details)
unitsHolder = view.findViewById(R.id.details_units_holder) unitsHolder = view.findViewById(R.id.details_units_holder)
val id = FurtherInfoFragmentArgs.fromBundle(requireArguments()).shiftId
editButton.setOnClickListener { 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?) { override fun onSuccess(data: Any?) {
@@ -91,7 +105,7 @@ class FurtherInfoFragment : BaseFragment<InfoViewModel>(R.layout.fragment_futher
unitsTV.text = units.toString() unitsTV.text = units.toString()
val paymentSummary = val paymentSummary =
StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ") StringBuilder().append(units.formatToTwoDp()).append(" Units @ ")
.append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n") .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n")
.append("Equals: ").append(totalPay.formatAsCurrencyString()) .append("Equals: ").append(totalPay.formatAsCurrencyString())
totalPayTV.text = paymentSummary totalPayTV.text = paymentSummary

View File

@@ -1,19 +1,15 @@
package com.appttude.h_mal.farmr.ui 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.os.Bundle
import android.view.Menu import android.view.MenuItem
import androidx.appcompat.widget.Toolbar 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.R
import com.appttude.h_mal.farmr.base.BackPressedListener
import com.appttude.h_mal.farmr.base.BaseActivity import com.appttude.h_mal.farmr.base.BaseActivity
import com.appttude.h_mal.farmr.utils.popBackStack
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
private lateinit var toolbar: Toolbar private lateinit var toolbar: Toolbar
private lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -21,26 +17,19 @@ class MainActivity : BaseActivity() {
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
val fragmentTransaction = supportFragmentManager.beginTransaction() navHost = supportFragmentManager
fragmentTransaction.replace(R.id.container, FragmentMain()).addToBackStack("main").commit() .findFragmentById(R.id.container) as NavHostFragment
val navController = navHost.navController
navController.setGraph(R.navigation.shift_navigation)
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Inflate the menu; this adds items to the action bar if it is present. // Handle action bar item clicks here. The action bar will
menuInflater.inflate(R.menu.menu_main, menu) // automatically handle clicks on the Home/Up button, so long
return true // as you specify a parent activity in AndroidManifest.xml.
} when (item.itemId) {
android.R.id.home -> onBackPressed()
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()
}
} }
return super.onOptionsItemSelected(item)
} }
} }

View File

@@ -2,24 +2,29 @@ package com.appttude.h_mal.farmr.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.os.Bundle import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.base.BaseListAdapter 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.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.model.ShiftType 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.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( class ShiftListAdapter(
private val fragment: Fragment, private val fragment: ChildFragment<*>,
emptyView: View, emptyView: View?,
private val longPressCallback: (Long) -> Unit private val viewModel: MainViewModel
) : BaseListAdapter<ShiftObject>(diffCallBack, R.layout.list_item_1, emptyView) { ) : BaseListAdapter<ShiftObject>(diffCallBack, R.layout.list_item_1, emptyView) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -30,70 +35,91 @@ class ShiftListAdapter(
val descriptionTextView: TextView = view.findViewById(R.id.location) val descriptionTextView: TextView = view.findViewById(R.id.location)
val dateTextView: TextView = view.findViewById(R.id.date) val dateTextView: TextView = view.findViewById(R.id.date)
val totalPay: TextView = view.findViewById(R.id.total_pay) 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) 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 when (getItemViewType(position)) {
dateTextView.text = dateText HOURLY_ITEM -> {
totalPay.text = totalPayText 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() val time = data.getHoursMinutesPairFromDuration()
hoursView.text = time.first 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() val unitsText: String = data.units.toString()
hoursView.text = unitsText unitsView.text = unitsText
h.text = ""
minutesView.text = ""
m.text = "pcs"
} }
} }
descriptionTextView.text = data.description
dateTextView.text = data.date
totalPay.text = data.totalPay.formatToTwoDpString()
val b: Bundle = Bundle()
b.putLong(ID, data.id)
view.setOnClickListener { view.setOnClickListener {
// Navigate to further info // Navigate to further info
fragment.navigateToFragment( val nav = FragmentMainDirections.mainToFurtherInfo(data.id)
FurtherInfoFragment(), fragment.navigateParent(nav)
bundle = b,
name = "furtherinfo"
)
} }
editView.setOnClickListener { editView.setOnClickListener {
// Navigate to edit //creating a popup menu
fragment.navigateToFragment( val popup = PopupMenu(it.context, it)
FragmentAddItem(), //inflating menu from xml resource
bundle = b, popup.inflate(R.menu.options_menu)
name = "additem"
) //adding click listener
} popup.setOnMenuItemClickListener { menu ->
view.setOnLongClickListener { when (menu.itemId) {
AlertDialog.Builder(it.context) R.id.update -> {
.setMessage("Are you sure you want to delete") // Navigate to edit
.setPositiveButton("delete") { _, _ -> longPressCallback.invoke(data.id) } val nav = FragmentMainDirections.mainToAddItem(data.id)
.setNegativeButton("cancel") { dialog, _ -> fragment.navigateParent(nav)
dialog?.dismiss() 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 { companion object {
val diffCallBack = object : DiffUtil.ItemCallback<ShiftObject>() { val diffCallBack = object : DiffUtil.ItemCallback<ShiftObject>() {
override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {

View File

@@ -4,4 +4,5 @@ const val LEGACY = "LEGACY_"
const val DATE_FORMAT = "yyyy-MM-dd" const val DATE_FORMAT = "yyyy-MM-dd"
const val TIME_FORMAT = "hh:mm" const val TIME_FORMAT = "hh:mm"
const val ID = "ID" const val ID = "ID"
const val SHIFT_ID = "shiftId"
const val CURRENCY = "£" const val CURRENCY = "£"

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.farmr.utils package com.appttude.h_mal.farmr.utils
import java.io.IOException import java.io.IOException
import java.math.RoundingMode
import java.text.NumberFormat import java.text.NumberFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@@ -26,7 +27,7 @@ fun Float.formatAsCurrencyString(): String? {
} }
fun Float.formatToTwoDpString(): String { fun Float.formatToTwoDpString(): String {
return toBigDecimal().setScale(2).toString() return toBigDecimal().setScale(2, RoundingMode.HALF_DOWN).toString()
} }
fun String.dateStringIsValid(): Boolean { fun String.dateStringIsValid(): Boolean {
@@ -47,6 +48,13 @@ fun String.convertDateString(format: String = DATE_FORMAT): Date? {
return formatter.parse(this) 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 * turns "HH:mm" into an hour and minutes pair
* *

View File

@@ -20,7 +20,7 @@ fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
* var s: String? * var s: String?
* i.validate{!i.isNullOrEmpty()} { print("string is empty") } * i.validate{!i.isNullOrEmpty()} { print("string is empty") }
*/ */
inline fun<T: Any?> T.validateField(validate: (T) -> Boolean, onError: () -> Unit) { inline fun <T : Any?> T.validateField(validate: (T) -> Boolean, onError: () -> Unit) {
if (!validate.invoke(this)) { if (!validate.invoke(this)) {
onError.invoke() onError.invoke()
} }
@@ -30,9 +30,25 @@ inline fun<T: Any?> 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 * 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. * The sort is stable. It means that equal elements preserve their order relative to each other after sorting.
*/ */
inline fun <T, R : Comparable<R>> Iterable<T>.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List<T> { inline fun <T, R : Comparable<R>> Iterable<T>.sortedByOrder(
order: Order = Order.ASCENDING,
crossinline selector: (T) -> R?
): List<T> {
return when (order) { return when (order) {
Order.ASCENDING -> sortedWith(compareBy(selector)) Order.ASCENDING -> sortedWith(compareBy(selector))
Order.DESCENDING -> sortedWith(compareByDescending(selector)) Order.DESCENDING -> sortedWith(compareByDescending(selector))
} }
}
/**
* Tries to retrieve a variable that may throw an exception
*
* @Returns variable if successful else null
*/
inline fun <T : Any?> tryGet(validate: () -> T?): T? {
return try {
validate.invoke()
} catch (e: Exception) {
null
}
} }

View File

@@ -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()

View File

@@ -10,13 +10,7 @@ class InfoViewModel(
repository: Repository repository: Repository
) : ShiftViewModel(repository) { ) : ShiftViewModel(repository) {
fun retrieveData(bundle: Bundle?) { fun retrieveData(id: Long) {
val id = bundle?.getLong(ID)
if (id == null) {
onError("Failed to retrieve shift")
return
}
val shift = getCurrentShift(id) val shift = getCurrentShift(id)
if (shift == null) { if (shift == null) {
onError("Failed to retrieve shift") onError("Failed to retrieve shift")

View File

@@ -1,7 +1,11 @@
package com.appttude.h_mal.farmr.viewmodel package com.appttude.h_mal.farmr.viewmodel
import android.graphics.Color
import android.graphics.drawable.Drawable
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer 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.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject 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_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.Sortable
import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.convertDateString 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.formatAsCurrencyString
import com.appttude.h_mal.farmr.utils.sortedByOrder import com.appttude.h_mal.farmr.utils.sortedByOrder
import jxl.Workbook import jxl.Workbook
@@ -30,6 +35,7 @@ import jxl.write.WritableWorkbook
import jxl.write.WriteException import jxl.write.WriteException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.Calendar
import java.util.Locale import java.util.Locale
@@ -42,8 +48,17 @@ class MainViewModel(
private var mSort: Sortable = Sortable.ID private var mSort: Sortable = Sortable.ID
private var mOrder: Order = Order.ASCENDING private var mOrder: Order = Order.ASCENDING
private var selectedItemId: Int? = null
private val observer = Observer<List<ShiftEntity>> { private val observer = Observer<List<ShiftEntity>> {
it?.let { updateFiltrationAndPostResults(it) } it?.let { updateFiltrationAndPostResults(it) }
private var selectedItemId: Int? = null
private val observer = Observer<List<ShiftObject>> {
it?.let {
val result = it.applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
} }
init { init {
@@ -103,7 +118,7 @@ class MainViewModel(
if (second == null) return compareDate.after(first) if (second == null) return compareDate.after(first)
if (first == null) return compareDate.before(second) 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() { fun clearFilters() {
super.setFiltrationDetails(null, null, null, null) val result = super.setFiltrationDetails(null, null, null, null)
onSuccess(Success("Filters have been cleared")) if (result) refreshLiveData()
refreshLiveData()
} }
/* /*
@@ -298,4 +312,25 @@ class MainViewModel(
return null return null
} }
fun retrieveEvents(): List<EventDay>? {
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<ShiftObject>? {
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
}
} }

View File

@@ -18,25 +18,13 @@ open class ShiftViewModel(
*/ */
fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) 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( open fun setFiltrationDetails(
description: String?, description: String?,
dateFrom: String?, dateFrom: String?,
dateTo: String?, dateTo: String?,
type: String? type: String?
) { ): Boolean {
repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) return repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type)
} }
open fun getFiltrationDetails(): FilterStore { open fun getFiltrationDetails(): FilterStore {

View File

@@ -289,6 +289,7 @@ class SubmissionViewModel(
description = description, description = description,
date = date, date = date,
units = units!!, units = units!!,
rateOfPay = rateOfPay, rateOfPay = rateOfPay,
) )
} }

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,5v14L5,19L5,5h14m1.1,-2L3.9,3c-0.5,0 -0.9,0.4 -0.9,0.9v16.2c0,0.4 0.4,0.9 0.9,0.9h16.2c0.4,0 0.9,-0.5 0.9,-0.9L21,3.9c0,-0.5 -0.5,-0.9 -0.9,-0.9zM11,7h6v2h-6L11,7zM11,11h6v2h-6v-2zM11,15h6v2h-6zM7,7h2v2L7,9zM7,11h2v2L7,13zM7,15h2v2L7,17z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="#656565"
android:orientation="horizontal"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:showDividers="end">
<LinearLayout
android:id="@+id/time_holder"
android:layout_width="@dimen/unit_holder_width"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="12dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/hours"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hours_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/minutes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/m"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/minutes_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/totalpay_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
app:layout_constraintRight_toRightOf="@id/time_holder"
app:layout_constraintTop_toBottomOf="@id/time_holder">
<TextView
android:id="@+id/currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pound_sign"
android:textColor="#728fcc"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/total_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="000.00"
android:textColor="#728fcc"
android:textSize="@dimen/total_pay_size" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"
app:layout_constraintLeft_toRightOf="@id/time_holder"
app:layout_constraintTop_toTopOf="@id/time_holder" />
<TextView
android:id="@+id/location"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:gravity="bottom|start"
android:textColor="#000000"
android:textSize="@dimen/location_size"
android:autoSizeMinTextSize="@dimen/location_autosize_min"
android:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/time_holder"
app:layout_constraintLeft_toRightOf="@id/line"
app:layout_constraintRight_toLeftOf="@id/imageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
android:maxLines="2"
tools:text="Location Name is quite long and with a second line and more" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:maxLines="3"
tools:text="01-05-2010"
android:textSize="@dimen/date_size"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/location"
app:layout_constraintLeft_toLeftOf="@id/location"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
android:layout_marginEnd="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="#656565"
android:orientation="horizontal"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:showDividers="end">
<LinearLayout
android:id="@+id/time_holder"
android:layout_width="@dimen/unit_holder_width"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="12dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/pieces"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="10.0"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/piece_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/totalpay_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
app:layout_constraintRight_toRightOf="@id/time_holder"
app:layout_constraintTop_toBottomOf="@id/time_holder">
<TextView
android:id="@+id/currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pound_sign"
android:textColor="#728fcc"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/total_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="000.00"
android:textColor="#728fcc"
android:textSize="@dimen/total_pay_size" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"
app:layout_constraintLeft_toRightOf="@id/time_holder"
app:layout_constraintTop_toTopOf="@id/time_holder" />
<TextView
android:id="@+id/location"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:gravity="bottom|start"
android:textColor="#000000"
android:textSize="@dimen/location_size"
android:autoSizeMinTextSize="@dimen/location_autosize_min"
android:autoSizeTextType="uniform"
app:layout_constraintBottom_toBottomOf="@id/time_holder"
app:layout_constraintLeft_toRightOf="@id/line"
app:layout_constraintRight_toLeftOf="@id/imageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
android:maxLines="2"
tools:text="Location Name is quite long and with a second line" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:maxLines="3"
tools:text="01-05-2010"
android:textSize="@dimen/date_size"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/location"
app:layout_constraintLeft_toLeftOf="@id/location"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
android:layout_marginEnd="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".ui.CalendarFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin">
<com.applandeo.materialcalendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:headerColor="@color/colorPrimary"
app:highlightedDaysLabelsColor="@color/colorPrimary"
app:selectionColor="@color/colorAccent"
app:type="classic" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/shifts_available_recycler"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:nestedScrollingEnabled="false"
tools:itemCount="5"
tools:listitem="@layout/list_cell_hourly" />
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,24 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.appttude.h_mal.farmr.ui.FragmentList">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_item_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/list_cell_hourly">
</androidx.recyclerview.widget.RecyclerView>
<include
android:layout_centerInParent="true"
android:visibility="visible"
layout="@layout/empty_list_view"
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>

View File

@@ -1,34 +1,39 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.appttude.h_mal.farmr.ui.FragmentMain"> tools:context="com.appttude.h_mal.farmr.ui.FragmentMain">
<androidx.recyclerview.widget.RecyclerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/list_item_view" android:id="@+id/sub_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toTopOf="@id/bottom_bar"
tools:listitem="@layout/list_item_1"> app:layout_constraintLeft_toLeftOf="parent"
</androidx.recyclerview.widget.RecyclerView> app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_calendar" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_navigation_menu" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab1" android:id="@+id/fab1"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentRight="true" app:layout_constraintRight_toRightOf="parent"
android:layout_alignParentBottom="true" app:layout_constraintBottom_toTopOf="@id/bottom_bar"
android:layout_margin="@dimen/fab_margin" android:layout_margin="@dimen/fab_margin"
android:contentDescription="@string/fab"
android:src="@drawable/add" android:src="@drawable/add"
app:backgroundTint="@color/colorPrimary" /> app:backgroundTint="@color/colorPrimary" />
<include </androidx.constraintlayout.widget.ConstraintLayout>
android:layout_centerInParent="true"
android:visibility="visible"
layout="@layout/empty_list_view"
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</RelativeLayout>

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="#656565"
android:orientation="horizontal"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:showDividers="end">
<LinearLayout
android:id="@+id/time_holder"
android:layout_width="@dimen/unit_holder_width"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="12dp"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/hours"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hours_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/minutes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/m"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/minutes_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/totalpay_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
app:layout_constraintRight_toRightOf="@id/time_holder"
app:layout_constraintTop_toBottomOf="@id/time_holder">
<TextView
android:id="@+id/currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pound_sign"
android:textColor="#728fcc"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/total_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="000.00"
android:textColor="#728fcc"
android:textSize="@dimen/total_pay_size" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"
app:layout_constraintLeft_toRightOf="@id/time_holder"
app:layout_constraintTop_toTopOf="@id/time_holder" />
<TextView
android:id="@+id/location"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:gravity="bottom|start"
android:textColor="#000000"
android:textSize="@dimen/location_size"
app:layout_constraintBottom_toBottomOf="@id/time_holder"
app:layout_constraintLeft_toRightOf="@id/line"
app:layout_constraintRight_toLeftOf="@id/imageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
android:maxLines="1"
tools:text="Location Name" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:maxLines="3"
tools:text="01-05-2010"
android:textSize="@dimen/date_size"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/location"
app:layout_constraintLeft_toLeftOf="@id/location"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
android:layout_marginEnd="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="#656565"
android:orientation="horizontal"
android:paddingTop="24dp"
android:paddingBottom="24dp"
android:showDividers="end">
<LinearLayout
android:id="@+id/time_holder"
android:layout_width="@dimen/unit_holder_width"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="12dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/pieces"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="10.0"
android:textColor="#143d66"
android:textSize="@dimen/unit_text_size" />
<TextView
android:id="@+id/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/piece_symbol"
android:textColor="#143d66"
android:textSize="@dimen/units_symbol_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/totalpay_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="horizontal"
app:layout_constraintRight_toRightOf="@id/time_holder"
app:layout_constraintTop_toBottomOf="@id/time_holder">
<TextView
android:id="@+id/currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pound_sign"
android:textColor="#728fcc"
android:textSize="@dimen/units_symbol_size" />
<TextView
android:id="@+id/total_pay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="000.00"
android:textColor="#728fcc"
android:textSize="@dimen/total_pay_size" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"
app:layout_constraintLeft_toRightOf="@id/time_holder"
app:layout_constraintTop_toTopOf="@id/time_holder" />
<TextView
android:id="@+id/location"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:gravity="bottom|start"
android:textColor="#000000"
android:textSize="@dimen/location_size"
app:layout_constraintBottom_toBottomOf="@id/time_holder"
app:layout_constraintLeft_toRightOf="@id/line"
app:layout_constraintRight_toLeftOf="@id/imageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
android:maxLines="1"
tools:text="Location Name" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:maxLines="3"
tools:text="01-05-2010"
android:textSize="@dimen/date_size"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/location"
app:layout_constraintLeft_toLeftOf="@id/location"
app:layout_constraintBottom_toBottomOf="@id/totalpay_holder"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/baseline_more_vert_24"
android:layout_marginEnd="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout" android:id="@+id/linearLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -8,10 +9,11 @@
android:orientation="horizontal" android:orientation="horizontal"
android:paddingBottom="24dp" android:paddingBottom="24dp"
android:paddingTop="24dp" android:paddingTop="24dp"
android:showDividers="end"> android:showDividers="end"
tools:ignore="HardcodedText">
<LinearLayout <LinearLayout
android:layout_width="87dp" android:layout_width="@dimen/unit_holder_width"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
@@ -30,7 +32,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="00" android:text="00"
android:textColor="#143d66" android:textColor="#143d66"
android:textSize="30sp" /> android:textSize="@dimen/unit_text_size" />
<TextView <TextView
android:id="@+id/h" android:id="@+id/h"
@@ -38,7 +40,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="h" android:text="h"
android:textColor="#143d66" android:textColor="#143d66"
android:textSize="12sp" /> android:textSize="@dimen/units_symbol_size" />
<TextView <TextView
android:id="@+id/minutes" android:id="@+id/minutes"
@@ -47,7 +49,7 @@
android:layout_weight="1" android:layout_weight="1"
android:text="00" android:text="00"
android:textColor="#143d66" android:textColor="#143d66"
android:textSize="30sp" /> android:textSize="@dimen/unit_text_size"/>
<TextView <TextView
android:id="@+id/m" android:id="@+id/m"
@@ -56,7 +58,7 @@
android:layout_weight="1" android:layout_weight="1"
android:text="m" android:text="m"
android:textColor="#143d66" android:textColor="#143d66"
android:textSize="12sp" /> android:textSize="@dimen/units_symbol_size" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -70,9 +72,9 @@
android:id="@+id/currency" android:id="@+id/currency"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="£" android:text="@string/pound_sign"
android:textColor="#728fcc" android:textColor="#728fcc"
android:textSize="12sp" /> android:textSize="@dimen/units_symbol_size" />
<TextView <TextView
android:id="@+id/total_pay" android:id="@+id/total_pay"
@@ -80,7 +82,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="000.00" android:text="000.00"
android:textColor="#728fcc" android:textColor="#728fcc"
android:textSize="20sp" /> android:textSize="@dimen/total_pay_size" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
@@ -106,7 +108,7 @@
android:layout_below="@+id/date" android:layout_below="@+id/date"
android:maxLines="3" android:maxLines="3"
android:text="Location Name" android:text="Location Name"
android:textSize="20sp" /> android:textSize="@dimen/location_size" />
<TextView <TextView
android:id="@+id/date" android:id="@+id/date"
@@ -119,7 +121,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="01-05-2010" android:text="01-05-2010"
android:textColor="#000000" android:textColor="#000000"
android:textSize="16sp" /> android:textSize="@dimen/date_size" />
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/imageView"

View File

@@ -32,14 +32,17 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<FrameLayout <androidx.fragment.app.FragmentContainerView
android:layout_below="@id/appbar" android:layout_below="@id/appbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:name="androidx.navigation.fragment.NavHostFragment"
app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:id="@+id/container"> app:defaultNavHost="true"
</FrameLayout> android:id="@+id/container"
tools:layout="@layout/fragment_main">
</androidx.fragment.app.FragmentContainerView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/nav_list"
app:showAsAction="always|withText"
android:icon="@drawable/baseline_list_alt_24"
android:title="@string/text_label_1"/>
<item
android:id="@+id/nav_calendar"
app:showAsAction="always|withText"
android:icon="@drawable/baseline_calendar_month_24"
android:title="@string/text_label_2"/>
</menu>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/update"
android:orderInCategory="100"
android:title="@string/update_shift"
app:showAsAction="never" />
<item
android:id="@+id/delete"
android:orderInCategory="100"
android:title="@string/delete_shift"
app:showAsAction="never" />
</menu>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/shift_navigation"
app:startDestination="@id/nav_list">
<fragment
android:id="@+id/nav_list"
android:name="com.appttude.h_mal.farmr.ui.FragmentList"
android:label="@string/text_label_1"
tools:layout="@layout/fragment_list" />
<fragment
android:id="@+id/nav_calendar"
android:name="com.appttude.h_mal.farmr.ui.CalendarFragment"
android:label="@string/text_label_2"
tools:layout="@layout/fragment_calendar" />
</navigation>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/shift_navigation"
app:startDestination="@id/fragmentMain">
<fragment
android:id="@+id/fragmentMain"
android:name="com.appttude.h_mal.farmr.ui.FragmentMain"
android:label="fragment_main"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/main_to_addItem"
app:destination="@id/fragmentAddItem"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/main_to_filterData"
app:destination="@id/filterDataFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
<action
android:id="@+id/main_to_furtherInfo"
app:destination="@id/furtherInfoFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
</fragment>
<fragment
android:id="@+id/fragmentAddItem"
android:name="com.appttude.h_mal.farmr.ui.FragmentAddItem"
android:label="fragment_add_item"
tools:layout="@layout/fragment_add_item">
<argument
android:name="shiftId"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/filterDataFragment"
android:name="com.appttude.h_mal.farmr.ui.FilterDataFragment"
android:label="fragment_filter_data"
tools:layout="@layout/fragment_filter_data" />
<fragment
android:id="@+id/furtherInfoFragment"
android:name="com.appttude.h_mal.farmr.ui.FurtherInfoFragment"
android:label="fragment_futher_info"
tools:layout="@layout/fragment_futher_info" >
<action
android:id="@+id/furtherInfo_to_AddItem"
app:destination="@id/fragmentAddItem"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
<argument
android:name="shiftId"
app:argType="long" />
</fragment>
</navigation>

View File

@@ -4,6 +4,13 @@
<dimen name="activity_vertical_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen> <dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen> <dimen name="appbar_padding_top">8dp</dimen>
<dimen name="unit_text_size">20sp</dimen>
<dimen name="units_symbol_size">12sp</dimen>
<dimen name="total_pay_size">16sp</dimen>
<dimen name="location_size">16sp</dimen>
<dimen name="date_size">14sp</dimen>
<dimen name="unit_holder_width">75dp</dimen>
<dimen name="location_autosize_min">12sp</dimen>
</resources> </resources>

View File

@@ -87,14 +87,18 @@
<string name="export">Export Data</string> <string name="export">Export Data</string>
<string name="sort">Sort</string> <string name="sort">Sort</string>
<string name="admob_unit_id">ca-app-pub-3406791512187471/7557456476</string>
<string name="banner_home_footer">ca-app-pub-3406791512187471~9541579845</string>
<string name="help">Help &amp; Support</string> <string name="help">Help &amp; Support</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="further_info_title">Shift Details</string> <string name="further_info_title">Shift Details</string>
<string name="insert_break_in_minutes">insert break in minutes</string> <string name="insert_break_in_minutes">insert break in minutes</string>
<string name="break_res">Break</string> <string name="break_res">Break</string>
<string name="hours_symbol">h</string>
<string name="minutes_symbol">m</string>
<string name="piece_symbol">pcs</string>
<string name="pound_sign">£</string>
<string name="delete_shift">Delete Shift</string>
<string name="update_shift">Update Shift</string>
<string name="fab">Floating action button</string>
<string name="text_label_1">Shifts</string>
<string name="text_label_2">Calendar</string>
</resources> </resources>

View File

@@ -56,12 +56,10 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
fun retrieveData_validBundleNoShift_successfulRetrieval() { fun retrieveData_validBundleNoShift_successfulRetrieval() {
// Arrange // Arrange
val id = anyLong() val id = anyLong()
val bundle = mockk<Bundle>()
// Act // Act
every { repository.readSingleShiftFromDatabase(id) }.returns(null) every { repository.readSingleShiftFromDatabase(id) }.returns(null)
every { bundle.getLong(ID) }.returns(id) viewModel.retrieveData(id)
viewModel.retrieveData(bundle)
// Assert // Assert
assertEquals( assertEquals(

View File

@@ -112,7 +112,7 @@ class MainViewModelTest {
val retrievedShifts = retrieveCurrentData() val retrievedShifts = retrieveCurrentData()
val description = viewModel.getInformation() 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()) every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter())
viewModel.clearFilters() viewModel.clearFilters()
val descriptionAfterClearedFilter = viewModel.getInformation() val descriptionAfterClearedFilter = viewModel.getInformation()

View File

@@ -13,6 +13,7 @@ buildscript {
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
} }
dependencies { dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$NAVIGATION_VERSION"
classpath "com.android.tools.build:gradle:$GRADLE_PLUGIN_VERSION" classpath "com.android.tools.build:gradle:$GRADLE_PLUGIN_VERSION"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
classpath "com.autonomousapps:dependency-analysis-gradle-plugin:$GRADLE_ANALYZE_VERSION" classpath "com.autonomousapps:dependency-analysis-gradle-plugin:$GRADLE_ANALYZE_VERSION"

View File

@@ -23,7 +23,7 @@ platform :android do
desc "Deploy a new version to the Google Play" desc "Deploy a new version to the Google Play"
lane :deploy do lane :deploy do
gradle(task: "clean assembleRelease") gradle(task: "clean bundle", build_type: "Release")
upload_to_play_store upload_to_play_store
end end
end end

View File

@@ -8,6 +8,7 @@ MATERIAL_VERSION = 1.0.0
CONSTR_LAYOUT_VERSION = 1.1.3 CONSTR_LAYOUT_VERSION = 1.1.3
LIFECYCLE_VERSION = 2.5.1 LIFECYCLE_VERSION = 2.5.1
VIEWMODEL_VERSION = 2.4.1 VIEWMODEL_VERSION = 2.4.1
NAVIGATION_VERSION = 2.3.2
PREFERENCES_VERSION = 1.2.1 PREFERENCES_VERSION = 1.2.1
MOKITO_INLINE_VERSION = 2.13.0 MOKITO_INLINE_VERSION = 2.13.0
CORE_TEST_VERSION = 2.1.0 CORE_TEST_VERSION = 2.1.0
@@ -26,8 +27,8 @@ KOTLIN_VERSION = 1.7.10
GRADLE_ANALYZE_VERSION = 1.20.0 GRADLE_ANALYZE_VERSION = 1.20.0
# Android configuration # Android configuration
COMPILE_SDK_VERSION = android-31 COMPILE_SDK_VERSION = android-33
TARGET_SDK_VERSION = 31 TARGET_SDK_VERSION = 33
MIN_SDK_VERSION = 21 MIN_SDK_VERSION = 21
# Gradle parameters # Gradle parameters