diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4175da6..342196f 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,26 +1,130 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
-# See: https://circleci.com/docs/configuration-reference
+# See: https://circleci.com/docs/2.0/configuration-reference
+# For a detailed guide to building and testing on Android, read the docs:
+# https://circleci.com/docs/2.0/language-android/ for more details.
version: 2.1
-# Define a job to be invoked later in a workflow.
-# See: https://circleci.com/docs/configuration-reference/#jobs
-jobs:
- say-hello:
- # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
- # See: https://circleci.com/docs/configuration-reference/#executor-job
- docker:
- - image: cimg/base:stable
- # Add steps to the job
- # See: https://circleci.com/docs/configuration-reference/#steps
+# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
+# See: https://circleci.com/docs/2.0/orb-intro/
+orbs:
+ android: circleci/android@2.3.0
+
+commands:
+ setup_repo:
+ description: checkout repo and android dependencies
steps:
- checkout
- run:
- name: "Say hello"
- command: "echo Hello, World!"
-
-# Orchestrate jobs using workflows
-# See: https://circleci.com/docs/configuration-reference/#workflows
+ name: Give gradle permissions
+ command: |
+ sudo chmod +x ./gradlew
+ - android/restore-gradle-cache
+ run_tests:
+ description: run tests for flavour specified
+ steps:
+ # The next step will run the unit tests
+ - run:
+ name: Run local unit tests
+ command: |
+ ./gradlew testDebugUnitTest
+ - android/save-gradle-cache
+ - store_artifacts:
+ path: app/build/reports
+ destination: reports
+ - store_test_results:
+ path: app/build/test-results
+ run_ui_tests:
+ description: run instrumentation and espresso tests
+ steps:
+ - android/start-emulator-and-run-tests:
+ post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest
+ test-command: ./gradlew connectedDebugAndroidTest --continue
+ system-image: system-images;android-26;google_apis;x86
+ # store screenshots for failed ui tests
+ - when:
+ condition: on_fail
+ steps:
+ - store_artifacts:
+ path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected
+ destination: connected_android_test
+ # store test reports
+ - store_artifacts:
+ path: app/build/reports/androidTests/connected
+ destination: reports
+ - store_test_results:
+ path: app/build/outputs/androidTest-results/connected
+ deploy_to_play_store:
+ description: deploy to playstore
+ steps:
+ # The next step will run the unit tests
+ - android/decode-keystore:
+ keystore-location: "./app/keystore.jks"
+ - run:
+ name: Setup playstore key
+ command: |
+ echo "$GOOGLE_PLAY_KEY" > "google-play-key.json"
+ - run:
+ name: Run fastlane command to deploy to playstore
+ command: |
+ pwd
+ bundle exec fastlane deploy
+ - store_test_results:
+ path: fastlane/report.xml
+# Define a job to be invoked later in a workflow.
+# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
+jobs:
+ # Below is the definition of your job to build and test your app, you can rename and customize it as you want.
+ build-and-test:
+ # These next lines define the Android machine image executor.
+ # See: https://circleci.com/docs/2.0/executor-types/
+ executor:
+ name: android/android-machine
+ tag: 2023.05.1
+ # Add steps to the job
+ # See: https://circleci.com/docs/2.0/configuration-reference/#steps
+ steps:
+ - setup_repo
+ - run_tests
+ run_instrumentation_test:
+ # These next lines define the Android machine image executor.
+ # See: https://circleci.com/docs/2.0/executor-types/
+ executor:
+ name: android/android-machine
+ tag: 2023.05.1
+ # Add steps to the job
+ # See: https://circleci.com/docs/2.0/configuration-reference/#steps
+ steps:
+ - setup_repo
+ - run_ui_tests
+ deploy-to-playstore:
+ docker:
+ - image: cimg/android:2023.07-browsers
+ auth:
+ username: ${DOCKER_USERNAME}
+ password: ${DOCKER_PASSWORD}
+ steps:
+ - setup_repo
+ - deploy_to_play_store
+# Invoke jobs via workflows
+# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
- say-hello-workflow:
+ version: 2
+ build-release:
jobs:
- - say-hello
+ - build-and-test:
+ context: appttude
+ - run_instrumentation_test:
+ context: appttude
+ filters:
+ branches:
+ only:
+ - master
+ - release
+ - deploy-to-playstore:
+ context: appttude
+ filters:
+ branches:
+ only:
+ - release
+ requires:
+ - build-and-test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 39fb081..ae7f176 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,97 @@
-*.iml
+### AndroidStudio ###
+# Covers files to be ignored for android development using Android Studio.
+
+# Built application files
+*.apk
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
.gradle
-/local.properties
-/.idea/workspace.xml
-/.idea/libraries
-.DS_Store
-/build
-/captures
+.gradle/
+build/
+
+# Signing files
+.signing/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio
+/*/build/
+/*/local.properties
+/*/out
+/*/*/build
+/*/*/production
+captures/
+.navigation/
+*.ipr
+*~
+*.swp
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Android Patch
+gen-external-apklibs
+
+# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
+
+# IntelliJ IDEA
+*.iml
+*.iws
+/out/
+
+# User-specific configurations
+.idea/androidTestResultsUserPreferences.xml
+.idea/caches/
+.idea/libraries/
+.idea/shelf/
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/.name
+.idea/compiler.xml
+.idea/copyright/profiles_settings.xml
+.idea/encodings.xml
+.idea/misc.xml
+.idea/modules.xml
+.idea/scopes/scope_settings.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+.idea/datasources.xml
+.idea/dataSources.ids
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+.idea/assetWizardSettings.xml
+.idea/gradle.xml
+.idea/jarRepositories.xml
+
+# Gem/fastlane
+Gemfile.lock
+/fastlane/report.xml
+# Google play files
+/google-play-key.json
diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
deleted file mode 100644
index 263c04c..0000000
--- a/.idea/androidTestResultsUserPreferences.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
deleted file mode 100644
index 0068d43..0000000
--- a/.idea/assetWizardSettings.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser
deleted file mode 100644
index 3f6cd0f..0000000
Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ
diff --git a/.idea/caches/gradle_models.ser b/.idea/caches/gradle_models.ser
deleted file mode 100644
index 5971202..0000000
Binary files a/.idea/caches/gradle_models.ser and /dev/null differ
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index ae78c11..0000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b589d56..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 1431050..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index eb2873e..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index fdf8d99..b1077fb 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 5ac489c..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 8c565d6..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..7a118b4
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"
diff --git a/app/build.gradle b/app/build.gradle
index 33df36f..19592a8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,23 +1,40 @@
apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
+apply plugin: 'kotlin-kapt'
+def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
+def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD")
+def relKeyAlias = System.getenv("RELEASE_KEY_ALIAS")
+
+def keystorePath = "/keystore.jks"
+def keystore = file(keystorePath).exists() ? file(keystorePath) : null
android {
compileSdkVersion 31
defaultConfig {
applicationId "com.appttude.h_mal.farmr"
minSdkVersion 21
targetSdkVersion 31
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ versionCode 2
+ versionName "2.0"
+ testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
vectorDrawables.useSupportLibrary = true
}
+ signingConfigs {
+ release {
+ storePassword relStorePassword
+ keyPassword relKeyPassword
+ keyAlias relKeyAlias
+ storeFile keystore
+ }
+ }
buildTypes {
release {
+ signingConfig signingConfigs.release
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ useLibrary 'android.test.mock'
}
dependencies {
@@ -29,8 +46,46 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.fragment:fragment-ktx:1.4.0'
+ implementation 'androidx.activity:activity-ktx:1.4.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
+ implementation 'androidx.preference:preference:1.2.1'
implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2'
- testImplementation 'junit:junit:4.12'
+ / * Unit testing * /
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
+ testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
+ / * mockito and livedata testing * /
+ testImplementation 'org.mockito:mockito-inline:2.13.0'
+ testImplementation 'androidx.arch.core:core-testing:2.1.0'
+ / * MockK * /
+ def mockk_ver = "1.10.5"
+ testImplementation "io.mockk:mockk:$mockk_ver"
+ androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
+ / * Android Espresso * /
+ def testJunitVersion = "1.1.5"
+ def testRunnerVersion = "1.5.2"
+ def espressoVersion = "3.5.1"
+ androidTestImplementation "androidx.test.ext:junit:$testJunitVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
+ androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"
+ implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
+ androidTestImplementation "androidx.test:runner:$testRunnerVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
+ androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
+ androidTestImplementation "org.hamcrest:hamcrest:2.2"
+ / * Room database * /
+ def room_version = "2.4.3"
+ implementation "androidx.room:room-runtime:$room_version"
+ kapt "androidx.room:room-compiler:$room_version"
+ implementation "androidx.room:room-ktx:$room_version"
+ / *Kodein Dependency Injection * /
+ def kodein_version = "6.2.1"
+ implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version"
+ implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
+ / * jxl * /
+ implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt
new file mode 100644
index 0000000..616c325
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt
@@ -0,0 +1,38 @@
+package com.appttude.h_mal.farmr.application
+
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.idling.CountingIdlingResource
+import androidx.test.platform.app.InstrumentationRegistry
+import com.appttude.h_mal.farmr.base.BaseApplication
+import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
+import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
+import com.appttude.h_mal.farmr.model.Shift
+
+class TestAppClass : BaseApplication() {
+ private val idlingResources = CountingIdlingResource("Data_loader")
+
+ lateinit var database: LegacyDatabase
+ lateinit var preferenceProvider: PreferenceProvider
+
+ override fun onCreate() {
+ super.onCreate()
+ IdlingRegistry.getInstance().register(idlingResources)
+ }
+
+ override fun createDatabase(): LegacyDatabase {
+ database =
+ LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver)
+ return database
+ }
+
+ override fun createPrefs(): PreferenceProvider {
+ preferenceProvider = PreferenceProvider(this)
+ return preferenceProvider
+ }
+
+ fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift)
+ fun addShiftsToDatabase(shifts: List) = shifts.forEach { addToDatabase(it) }
+ fun clearDatabase() = database.deleteAllShiftsInDatabase()
+ fun cleanPrefs() = preferenceProvider.clearPrefs()
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt
new file mode 100644
index 0000000..ee712d4
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt
@@ -0,0 +1,21 @@
+package com.appttude.h_mal.farmr.application
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+
+class TestRunner : AndroidJUnitRunner() {
+ @Throws(
+ InstantiationException::class,
+ IllegalAccessException::class,
+ ClassNotFoundException::class
+ )
+ override fun newApplication(
+ cl: ClassLoader?,
+ className: String?,
+ context: Context?
+ ): Application {
+ return super.newApplication(cl, TestAppClass::class.java.name, context)
+ }
+
+}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt
index d8872a5..31f500e 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt
@@ -3,21 +3,23 @@ package com.appttude.h_mal.farmr.data
import android.content.ContentResolver
import android.content.ContentValues
import androidx.test.rule.provider.ProviderTestRule
-import com.appttude.h_mal.farmr.data.ShiftsContract.CONTENT_AUTHORITY
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.CONTENT_URI
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry._ID
+import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.CONTENT_AUTHORITY
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
+import org.junit.After
import org.junit.Rule
import org.junit.Test
@@ -30,6 +32,11 @@ class ShiftProviderTest {
private val contentResolver: ContentResolver
get() = providerRule.resolver
+ @After
+ fun tearDown() {
+ contentResolver.delete(CONTENT_URI, null, null)
+ }
+
@Test
fun insertEntry_queryEntry_assertEntry() {
// Arrange
@@ -75,6 +82,8 @@ class ShiftProviderTest {
// Assert
val item = contentResolver.query(CONTENT_URI, projection, null, null, null)
item?.takeIf { it.moveToNext() }?.run {
+ val id = getLong(getColumnIndexOrThrow(_ID))
+
val descriptionColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DESCRIPTION))
val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE))
val timeInColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN))
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt
new file mode 100644
index 0000000..e539bdd
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt
@@ -0,0 +1,91 @@
+package com.appttude.h_mal.farmr.data.legacydb
+
+import androidx.test.rule.provider.ProviderTestRule
+import com.appttude.h_mal.farmr.model.Shift
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class LegacyDatabaseTest {
+ @get:Rule
+ val providerRule: ProviderTestRule = ProviderTestRule
+ .Builder(ShiftProvider::class.java, ShiftsContract.CONTENT_AUTHORITY)
+ .build()
+
+ private lateinit var database: LegacyDatabase
+
+ @Before
+ fun setup() {
+ database = LegacyDatabase(providerRule.resolver)
+ }
+
+ @After
+ fun tearDown() {
+ database.deleteAllShiftsInDatabase()
+ }
+
+ @Test
+ fun insertShift_readShift_successfulRead() {
+ // Arrange
+ val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
+
+ // Act
+ database.insertShiftDataIntoDatabase(shift)
+ val retrievedShift = database.readShiftsFromDatabase()?.first()
+
+ // Assert
+ assertEquals(retrievedShift?.description, shift.description)
+ assertEquals(retrievedShift?.date, shift.date)
+ assertEquals(retrievedShift?.units, shift.units)
+ assertEquals(retrievedShift?.rateOfPay, shift.rateOfPay)
+ }
+
+ @Test
+ fun insertShift_updateShift_successfulRead() {
+ // Arrange
+ val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
+ val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f)
+
+ // Act
+ database.insertShiftDataIntoDatabase(shift)
+ val id = database.readShiftsFromDatabase()?.first()!!.id
+ database.updateShiftDataIntoDatabase(
+ id = id,
+ typeString = updateShift.type.type,
+ descriptionString = updateShift.description,
+ dateString = updateShift.date,
+ timeInString = updateShift.timeIn ?: "",
+ timeOutString = updateShift.timeOut ?: "",
+ duration = updateShift.duration ?: 0f,
+ breaks = updateShift.breakMins ?: 0,
+ units = updateShift.units!!,
+ payRate = updateShift.rateOfPay,
+ totalPay = updateShift.totalPay
+ )
+ val retrievedShift = database.readSingleShiftWithId(id)
+
+ // Assert
+ assertEquals(retrievedShift?.description, updateShift.description)
+ assertEquals(retrievedShift?.date, updateShift.date)
+ assertEquals(retrievedShift?.units, updateShift.units)
+ assertEquals(retrievedShift?.rateOfPay, updateShift.rateOfPay)
+ }
+
+ @Test
+ fun insertShift_deleteShift_databaseEmpty() {
+ // Arrange
+ val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
+ val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f)
+
+ // Act
+ database.insertShiftDataIntoDatabase(shift)
+ database.insertShiftDataIntoDatabase(updateShift)
+ val id = database.readShiftsFromDatabase()?.first()!!.id
+ database.deleteSingleShift(id)
+
+ // Assert
+ assertEquals(database.readShiftsFromDatabase()?.size, 1)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
new file mode 100644
index 0000000..8d4ebb8
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
@@ -0,0 +1,107 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import androidx.test.core.app.ActivityScenario
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.RootMatchers.withDecorView
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import com.appttude.h_mal.farmr.application.TestAppClass
+import com.appttude.h_mal.farmr.ui.utils.getShifts
+import kotlinx.coroutines.runBlocking
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.kodein.di.android.kodein
+
+@Suppress("EmptyMethod")
+open class BaseTest(
+ private val activity: Class,
+ private val intentBundle: Bundle? = null,
+) {
+
+ lateinit var scenario: ActivityScenario
+ private lateinit var testApp: TestAppClass
+ private lateinit var testActivity: Activity
+ private lateinit var decorView: View
+
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE)
+
+ @Before
+ open fun setUp() {
+ val startIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity)
+ if (intentBundle != null) {
+ startIntent.replaceExtras(intentBundle)
+ }
+
+ testApp =
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
+ kodein(testApp)
+ runBlocking {
+ beforeLaunch()
+ }
+
+ scenario = ActivityScenario.launch(startIntent)
+ scenario.onActivity {
+ testApp =
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
+ onLaunch()
+ decorView = it.window.decorView
+ testActivity = it
+ }
+ afterLaunch()
+ }
+
+ fun getActivity() = testActivity
+
+ @After
+ fun tearDown() {
+ testFinished()
+ }
+
+ open fun beforeLaunch() {}
+ open fun onLaunch() {}
+ open fun afterLaunch() {}
+ open fun testFinished() {}
+
+ fun waitFor(delay: Long) {
+ Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction {
+ override fun getConstraints(): Matcher = ViewMatchers.isRoot()
+ override fun getDescription(): String = "wait for $delay milliseconds"
+ override fun perform(uiController: UiController, v: View?) {
+ uiController.loopMainThreadForAtLeast(delay)
+ }
+ })
+ }
+
+ @Suppress("DEPRECATION")
+ fun checkToastMessage(message: String) {
+ Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView)))
+ .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ waitFor(3500)
+ }
+ }
+
+ fun navigateBack() = Espresso.pressBack()
+
+ fun addRandomShifts() {
+ testApp.addShiftsToDatabase(getShifts())
+ }
+
+ fun clearDataBase() = testApp.clearDatabase()
+ fun clearPrefs() = testApp.cleanPrefs()
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt
new file mode 100644
index 0000000..a85098f
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt
@@ -0,0 +1,219 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.content.res.Resources
+import android.view.View
+import android.widget.DatePicker
+import android.widget.TimePicker
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.AppCompatButton
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.espresso.Espresso.onData
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.swipeDown
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.PickerActions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.platform.app.InstrumentationRegistry
+import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView
+import org.hamcrest.CoreMatchers.allOf
+import org.hamcrest.CoreMatchers.anything
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+
+@SuppressWarnings("unused")
+open class BaseTestRobot {
+
+ fun fillEditText(resId: Int, text: String?): ViewInteraction =
+ onView(withId(resId)).perform(
+ ViewActions.replaceText(text),
+ ViewActions.closeSoftKeyboard()
+ )
+
+ fun clickButton(resId: Int): ViewInteraction =
+ onView((withId(resId))).perform(click())
+
+// fun clickMenu(menuId: Int): ViewInteraction = onView()
+
+ fun matchView(resId: Int): ViewInteraction = onView(withId(resId))
+
+ fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId))
+
+ fun matchDisplayed(resId: Int): ViewInteraction = matchView(resId).check(matches(isDisplayed()))
+
+ fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
+ .check(matches(withText(text)))
+
+ fun matchText(viewId: Int, textId: Int): ViewInteraction = onView(withId(viewId))
+ .check(matches(withText(textId)))
+
+ fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text)
+
+ fun clickListItem(listRes: Int, position: Int) {
+ onData(anything())
+ .inAdapterView(allOf(withId(listRes)))
+ .atPosition(position).perform(click())
+ }
+
+ fun clickOnMenuItem(menuId: Int) {
+ openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context)
+ onView(withText(menuId)).perform(click())
+ }
+
+ fun clickDialogButton(text: String) {
+ onView(withText(text)).inRoot(isDialog())
+ .check(matches(isDisplayed()))
+ .perform(click());
+ }
+
+ fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(text))
+ )
+ )
+ }
+
+ fun scrollToRecyclerItem(
+ recyclerId: Int,
+ resIdForString: Int
+ ): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(resIdForString))
+ )
+ )
+ }
+
+ fun scrollToRecyclerItemByPosition(
+ recyclerId: Int,
+ position: Int
+ ): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollToPosition(position)
+ )
+ }
+
+ fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
+ matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(withText(resIdForString)),
+ click()
+ )
+ )
+ }
+
+ fun clickRecyclerAtPosition(recyclerId: Int, position: Int) {
+ matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollToPosition(position),
+ RecyclerViewActions.actionOnItemAtPosition(position, click()),
+ )
+ }
+
+ fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) {
+ matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollToPosition(position),
+ RecyclerViewActions.actionOnItemAtPosition(position, object : ViewAction {
+ override fun getDescription(): String {
+ return "click on subview in RecyclerView at position: $position"
+ }
+
+ override fun getConstraints(): Matcher {
+ return Matchers.allOf(
+ isAssignableFrom(
+ RecyclerView::class.java
+ ), isDisplayed()
+ )
+ }
+
+ override fun perform(uiController: UiController?, view: View?) {
+ view?.findViewById(subViewId)?.performClick()
+ }
+
+ }),
+ )
+ }
+
+ fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) {
+ matchView(recyclerId).perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(text))
+ ),
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(withText(text)),
+ click()
+ )
+ )
+ }
+
+ fun swipeDown(resId: Int): ViewInteraction =
+ onView(withId(resId)).perform(swipeDown())
+
+ fun getStringFromResource(@StringRes resId: Int): String =
+ Resources.getSystem().getString(resId)
+
+ fun pullToRefresh(resId: Int) {
+ onView(allOf(withId(resId), isDisplayed())).perform(swipeDown())
+ }
+
+ fun selectDateInPicker(year: Int, month: Int, day: Int) {
+ onView(withClassName(equalTo(DatePicker::class.java.name))).perform(
+ PickerActions.setDate(
+ year,
+ month,
+ day
+ )
+ )
+ onView(
+ allOf(
+ withClassName(equalTo(AppCompatButton::class.java.name)),
+ withText("OK")
+ )
+ ).perform(
+ click()
+ )
+ }
+
+ fun selectTextInSpinner(id: Int, text: String) {
+ clickButton(id)
+ onView(withSpinnerText(text)).perform(click())
+ }
+
+ fun selectTimeInPicker(hours: Int, minutes: Int) {
+ onView(withClassName(equalTo(TimePicker::class.java.name))).perform(
+ PickerActions.setTime(
+ hours, minutes
+ )
+ )
+ onView(
+ allOf(
+ withClassName(equalTo(AppCompatButton::class.java.name)),
+ withText("OK")
+ )
+ ).perform(
+ click()
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt
new file mode 100644
index 0000000..c28cfb1
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt
@@ -0,0 +1,41 @@
+package com.appttude.h_mal.farmr.ui.robots
+
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.ui.BaseTestRobot
+
+fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() }
+class AddItemScreenRobot : BaseTestRobot() {
+
+ fun clickShiftType(type: ShiftType) {
+ when (type) {
+ ShiftType.HOURLY -> clickButton(R.id.hourly)
+ ShiftType.PIECE -> clickButton(R.id.piecerate)
+ }
+ }
+
+ fun setDescription(text: String?) = fillEditText(R.id.locationEditText, text)
+ fun setDate(year: Int, month: Int, day: Int) {
+ clickButton(R.id.dateEditText)
+ selectDateInPicker(year, month, day)
+ }
+
+ fun setTimeIn(hour: Int, minutes: Int) {
+ clickButton(R.id.timeInEditText)
+ selectTimeInPicker(hour, minutes)
+ }
+
+ fun setTimeOut(hour: Int, minutes: Int) {
+ clickButton(R.id.timeOutEditText)
+ selectTimeInPicker(hour, minutes)
+ }
+
+ fun setBreakTime(mins: Int) = fillEditText(R.id.breakEditText, mins.toString())
+ fun setUnits(units: Float) = fillEditText(R.id.unitET, units.toString())
+ fun setRateOfPay(rateOfPay: Float) = fillEditText(R.id.payrateET, rateOfPay.toString())
+ fun submit() = clickButton(R.id.submit)
+
+ fun assertTotalPay(pay: String) = matchText(R.id.totalpayval, pay)
+ fun assertDuration(duration: String) = matchText(R.id.ShiftDuration, duration)
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt
new file mode 100644
index 0000000..acd6f63
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt
@@ -0,0 +1,29 @@
+package com.appttude.h_mal.farmr.ui.robots
+
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.ui.BaseTestRobot
+
+fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() }
+class FilterScreenRobot : BaseTestRobot() {
+
+ fun setDescription(text: String?) = fillEditText(R.id.filterLocationEditText, text)
+
+ fun setDateIn(year: Int, month: Int, day: Int) {
+ clickButton(R.id.fromdateInEditText)
+ selectDateInPicker(year, month, day)
+ }
+
+ fun setDateOut(year: Int, month: Int, day: Int) {
+ clickButton(R.id.filterDateOutEditText)
+ selectDateInPicker(year, month, day)
+ }
+
+ fun setType(type: ShiftType?) = when(type) {
+ ShiftType.HOURLY -> selectTextInSpinner(R.id.TypeFilterEditText, type.type)
+ ShiftType.PIECE -> selectTextInSpinner(R.id.TypeFilterEditText, type.type)
+ null -> selectTextInSpinner(R.id.TypeFilterEditText, "")
+ }
+ fun submit() = clickButton(R.id.submitFiltered)
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt
new file mode 100644
index 0000000..81bf480
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt
@@ -0,0 +1,28 @@
+package com.appttude.h_mal.farmr.ui.robots
+
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.Sortable
+import com.appttude.h_mal.farmr.ui.BaseTestRobot
+
+fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
+class HomeScreenRobot : BaseTestRobot() {
+
+ fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text)
+ fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition(R.id.list_item_view, position)
+ fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position, R.id.imageView)
+ fun clickFab() = clickButton(R.id.fab1)
+ fun clickOnInfoIcon() = clickButton(R.id.action_favorite)
+ fun clickFilterInMenu() = clickOnMenuItem(R.string.filter)
+ fun clickClearFilterInMenu() = clickOnMenuItem(R.string.clear)
+ fun clickSortInMenu() = clickOnMenuItem(R.string.sort)
+
+ fun applySort(sortable: Sortable, order: Order = Order.ASCENDING) {
+ clickSortInMenu()
+ val label = sortable.label
+ clickDialogButton(label)
+ val orderLabel = order.label
+ clickDialogButton(orderLabel)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt
new file mode 100644
index 0000000..b56d799
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt
@@ -0,0 +1,33 @@
+package com.appttude.h_mal.farmr.ui.robots
+
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.ui.BaseTestRobot
+
+fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() }
+class ViewItemScreenRobot : BaseTestRobot() {
+
+ fun matchShiftType(type: ShiftType) {
+ when (type) {
+ ShiftType.HOURLY -> matchText(R.id.details_shift, type.type)
+ ShiftType.PIECE -> matchText(R.id.details_shift, type.type)
+ }
+ }
+
+ fun matchDescription(text: String) = matchText(R.id.details_desc, text)
+ fun matchDate(date: String) {
+ matchText(R.id.details_date, date)
+ }
+
+ fun matchTime(timeIn: String, timeOut: String) {
+ matchText(R.id.details_time, "$timeIn-$timeOut")
+ }
+
+ fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, mins.toString())
+ fun matchUnits(units: Float) = fillEditText(R.id.details_units, units.toString())
+ fun matchRateOfPay(rateOfPay: Float) = fillEditText(R.id.details_pay_rate, rateOfPay.toString())
+ fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay)
+ fun matchDuration(duration: String) = matchText(R.id.details_duration, duration)
+
+ fun clickEdit() = clickButton(R.id.details_edit)
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt
new file mode 100644
index 0000000..a7decde
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt
@@ -0,0 +1,141 @@
+package com.appttude.h_mal.farmr.ui.tests
+
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.model.Sortable
+import com.appttude.h_mal.farmr.ui.BaseTest
+import com.appttude.h_mal.farmr.ui.MainActivity
+import com.appttude.h_mal.farmr.ui.robots.addScreen
+import com.appttude.h_mal.farmr.ui.robots.filterScreen
+import com.appttude.h_mal.farmr.ui.robots.homeScreen
+import com.appttude.h_mal.farmr.ui.robots.viewScreen
+import org.junit.Test
+
+class ShiftTests : BaseTest(MainActivity::class.java) {
+
+ override fun afterLaunch() {
+ super.afterLaunch()
+ addRandomShifts()
+
+ // Content resolver hard to mock
+ // Dirty technique to have a populated list
+ homeScreen {
+ clickFab()
+ navigateBack()
+ }
+ }
+
+ override fun testFinished() {
+ super.testFinished()
+ clearDataBase()
+ clearPrefs()
+ }
+
+ // Add a shift successfully
+ @Test
+ fun openAddScreen_addNewShift_newShiftCreated() {
+ homeScreen {
+ clickFab()
+ }
+ addScreen {
+ setDescription("This is a description")
+ setDate(2023, 2, 11)
+ clickShiftType(ShiftType.HOURLY)
+ setTimeIn(12, 0)
+ setTimeOut(14, 30)
+ setBreakTime(30)
+ setRateOfPay(10f)
+ assertDuration("2.0 hours")
+ assertTotalPay("£20.00")
+ submit()
+ }
+ homeScreen {
+ clickOnItemWithText("This is a description")
+ }
+ }
+
+ // Edit a shift successfully
+ @Test
+ fun test2() {
+ homeScreen {
+ clickOnEdit(0)
+ }
+ addScreen {
+ setDescription("Edited this shift")
+ setTimeIn(12, 0)
+ setTimeOut(14, 30)
+ setBreakTime(30)
+ setRateOfPay(20f)
+ assertDuration("2.0 hours")
+ assertTotalPay("£40.00")
+ submit()
+ }
+ homeScreen {
+ clickOnItemWithText("Edited this shift")
+ }
+ viewScreen {
+ matchDescription("Edited this shift")
+ matchDuration("2 Hours 0 Minutes (+ 30 minutes break)")
+ matchTotalPay("2.0 Hours @ £20.00 per Hour\nEquals: £40.00")
+ }
+ }
+
+ // filter the list with date from
+ @Test
+ fun test3() {
+ homeScreen {
+ applySort(Sortable.TYPE, Order.DESCENDING)
+ clickOnItemAtPosition(0)
+ viewScreen {
+ matchDescription("Day five")
+ matchShiftType(ShiftType.PIECE)
+ }
+ }
+ }
+
+ // filter the list with date to
+ @Test
+ fun test4() {
+ homeScreen {
+ clickFilterInMenu()
+ }
+ filterScreen {
+ setDateIn(2023,8,3)
+ setDateOut(2023,8,6)
+ submit()
+ }
+ homeScreen {
+ clickOnItemAtPosition(0)
+ }
+ }
+
+ // Add a shift as piece rate
+ @Test
+ fun test5() {
+ homeScreen {
+ clickFab()
+ }
+ addScreen {
+ setDescription("This is a description")
+ setDate(2023, 2, 11)
+ clickShiftType(ShiftType.PIECE)
+ setRateOfPay(10f)
+ setUnits(1f)
+ assertTotalPay("£10.00")
+ submit()
+ }
+ homeScreen {
+ clickOnItemWithText("This is a description")
+ }
+ }
+
+ // Validate the details screen
+ @Test
+ fun test6() {
+ }
+
+ // filter, sort, order and then reset
+ @Test
+ fun test7() {
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt
new file mode 100644
index 0000000..ad69acd
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt
@@ -0,0 +1 @@
+package com.appttude.h_mal.farmr.ui.utils
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt
new file mode 100644
index 0000000..0e05bb6
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt
@@ -0,0 +1,103 @@
+package com.appttude.h_mal.farmr.ui.utils
+
+import com.appttude.h_mal.farmr.model.Shift
+import com.appttude.h_mal.farmr.model.ShiftType
+
+fun getShifts() = listOf(
+ Shift(
+ ShiftType.HOURLY,
+ "Day one",
+ "2023-08-01",
+ "12:00",
+ "13:00",
+ 1f,
+ 0,
+ 0f,
+ 10f,
+ 10f
+ ),
+ Shift(
+ ShiftType.HOURLY,
+ "Day two",
+ "2023-08-02",
+ "12:00",
+ "13:00",
+ 1f,
+ 0,
+ 0f,
+ 10f,
+ 10f
+ ),
+ Shift(
+ ShiftType.HOURLY,
+ "Day three",
+ "2023-08-03",
+ "12:00",
+ "13:00",
+ 1f,
+ 30,
+ 0f,
+ 10f,
+ 5f
+ ),
+ Shift(
+ ShiftType.HOURLY,
+ "Day four",
+ "2023-08-04",
+ "12:00",
+ "13:00",
+ 1f,
+ 30,
+ 0f,
+ 10f,
+ 5f
+ ),
+ Shift(
+ ShiftType.PIECE,
+ "Day five",
+ "2023-08-05",
+ "",
+ "",
+ 0f,
+ 0,
+ 1f,
+ 10f,
+ 10f
+ ),
+ Shift(
+ ShiftType.PIECE,
+ "Day six",
+ "2023-08-06",
+ "",
+ "",
+ 0f,
+ 0,
+ 1f,
+ 10f,
+ 10f
+ ),
+ Shift(
+ ShiftType.PIECE,
+ "Day seven",
+ "2023-08-07",
+ "",
+ "",
+ 0f,
+ 0,
+ 1f,
+ 10f,
+ 10f
+ ),
+ Shift(
+ ShiftType.PIECE,
+ "Day eight",
+ "2023-08-08",
+ "",
+ "",
+ 0f,
+ 0,
+ 1f,
+ 10f,
+ 10f
+ )
+)
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt
new file mode 100644
index 0000000..a2ebc7a
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt
@@ -0,0 +1,123 @@
+package com.appttude.h_mal.farmr.ui.utils
+
+import android.os.SystemClock.sleep
+import android.view.View
+import android.widget.CheckBox
+import android.widget.Checkable
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.NoMatchingViewException
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.util.TreeIterables
+import org.hamcrest.BaseMatcher
+import org.hamcrest.CoreMatchers.isA
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+
+
+object EspressoHelper {
+
+ /**
+ * Perform action of waiting for a certain view within a single root view
+ * @param viewMatcher Generic Matcher used to find our view
+ */
+ fun searchFor(viewMatcher: Matcher): ViewAction {
+
+ return object : ViewAction {
+
+ override fun getConstraints(): Matcher = isRoot()
+ override fun getDescription(): String {
+ return "searching for view $this in the root view"
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ var tries = 0
+ val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view)
+
+ // Look for the match in the tree of child views
+ childViews.forEach {
+ tries++
+ if (viewMatcher.matches(it)) {
+ // found the view
+ return
+ }
+ }
+
+ throw NoMatchingViewException.Builder()
+ .withRootView(view)
+ .withViewMatcher(viewMatcher)
+ .build()
+ }
+ }
+ }
+
+ /**
+ * Performs an action to check/uncheck a checkbox
+ *
+ */
+ fun setChecked(checked: Boolean): ViewAction {
+ return object : ViewAction {
+ override fun getConstraints(): BaseMatcher {
+ return object : BaseMatcher() {
+ override fun describeTo(description: Description?) {}
+
+ override fun matches(actual: Any?): Boolean {
+ return isA(CheckBox::class.java).matches(actual)
+ }
+ }
+ }
+
+ override fun getDescription(): String {
+ return ""
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ val checkableView = view as Checkable
+ checkableView.isChecked = checked
+ }
+ }
+ }
+
+ /**
+ * Perform action of implicitly waiting for a certain view.
+ * This differs from EspressoExtensions.searchFor in that,
+ * upon failure to locate an element, it will fetch a new root view
+ * in which to traverse searching for our @param match
+ *
+ * @param viewMatcher ViewMatcher used to find our view
+ */
+ fun waitForView(
+ viewMatcher: Matcher,
+ waitMillis: Int = 5000,
+ waitMillisPerTry: Long = 100
+ ): ViewInteraction {
+
+ // Derive the max tries
+ val maxTries = waitMillis / waitMillisPerTry.toInt()
+
+ var tries = 0
+
+ for (i in 0..maxTries)
+ try {
+ // Track the amount of times we've tried
+ tries++
+
+ // Search the root for the view
+ onView(isRoot()).perform(searchFor(viewMatcher))
+
+ // If we're here, we found our view. Now return it
+ return onView(viewMatcher)
+
+ } catch (e: Exception) {
+
+ if (tries == maxTries) {
+ throw e
+ }
+ sleep(waitMillisPerTry)
+ }
+
+ throw Exception("Error finding a view matching $viewMatcher")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt
new file mode 100644
index 0000000..d398697
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt
@@ -0,0 +1,32 @@
+package com.appttude.h_mal.farmr.ui.utils
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+fun LiveData.getOrAwaitValue(
+ time: Long = 2,
+ timeUnit: TimeUnit = TimeUnit.SECONDS
+): T {
+ var data: T? = null
+ val latch = CountDownLatch(1)
+ val observer = object : Observer {
+ override fun onChanged(o: T?) {
+ data = o
+ latch.countDown()
+ this@getOrAwaitValue.removeObserver(this)
+ }
+ }
+
+ this.observeForever(observer)
+
+ // Don't wait indefinitely if the LiveData is not set.
+ if (!latch.await(time, timeUnit)) {
+ throw TimeoutException("LiveData value was never set.")
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ return data as T
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8dc1d6f..6fc2773 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt
deleted file mode 100644
index 78945fb..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.app.DatePickerDialog
-import android.os.Bundle
-import android.text.TextUtils
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ArrayAdapter
-import android.widget.Button
-import android.widget.EditText
-import android.widget.Spinner
-import androidx.fragment.app.Fragment
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
-import java.text.MessageFormat
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Date
-
-class FilterDataFragment : Fragment() {
- private val spinnerList: Array = arrayOf("", "Hourly", "Piece Rate")
- private val listArgs: MutableList = ArrayList()
- private var LocationET: EditText? = null
- private var dateFromET: EditText? = null
- private var dateToET: EditText? = null
- private var typeSpinner: Spinner? = null
- lateinit var mcurrentDate: Calendar
- private lateinit var activity: MainActivity
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View {
- val activity = (activity)
-
- // Inflate the layout for this fragment
- val rootView: View = inflater.inflate(R.layout.fragment_filter_data, container, false)
- activity.setActionBarTitle(getString(R.string.title_activity_filter_data))
- mcurrentDate = Calendar.getInstance()
- LocationET = rootView.findViewById(R.id.filterLocationEditText) as EditText?
- dateFromET = rootView.findViewById(R.id.fromdateInEditText) as EditText?
- dateToET = rootView.findViewById(R.id.filterDateOutEditText) as EditText?
- typeSpinner = rootView.findViewById(R.id.TypeFilterEditText) as Spinner?
-
-
- if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?")) {
- var str: String = activity.args!!.get(2)
- str = str.replace("%", "")
- LocationET!!.setText(str)
- }
- if (activity.selection != null && !(activity.args!!.get(0) == "2000-01-01")) {
- dateFromET!!.setText(activity.args!!.get(0))
- }
- if ((activity.selection != null) && (activity.args != null) && !(activity.args!![1] == (mcurrentDate.get(Calendar.YEAR).toString() + "-"
- + String.format("%02d", (mcurrentDate.get(Calendar.MONTH) + 1)) + "-"
- + String.format("%02d", mcurrentDate.get(Calendar.DAY_OF_MONTH))).toString())) {
- dateToET!!.setText(activity.args!![1])
- }
- dateFromET!!.setOnClickListener { //To show current date in the datepicker
- var mYear: Int = mcurrentDate.get(Calendar.YEAR)
- var mMonth: Int = mcurrentDate.get(Calendar.MONTH)
- var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH)
- if (!(dateFromET!!.text.toString() == "")) {
- val dateString: String = dateFromET!!.text.toString().trim()
- mYear = dateString.substring(0, 4).toInt()
- mMonth = dateString.substring(5, 7).toInt()
- if (mMonth == 1) {
- mMonth = 0
- } else {
- mMonth = mMonth - 1
- }
- mDay = dateString.substring(8).toInt()
- }
-
- val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday ->
- val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday))
- dateFromET!!.setText(input)
- }, mYear, mMonth, mDay)
- mDatePicker.setTitle("Select date")
- mDatePicker.show()
- }
- dateToET!!.setOnClickListener { //To show current date in the datepicker
- val mcurrentDate: Calendar = Calendar.getInstance()
- var mYear: Int = mcurrentDate.get(Calendar.YEAR)
- var mMonth: Int = mcurrentDate.get(Calendar.MONTH)
- var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH)
- if (!(dateToET!!.text.toString() == "")) {
- val dateString: String = dateToET!!.text.toString().trim({ it <= ' ' })
- mYear = dateString.substring(0, 4).toInt()
- mMonth = dateString.substring(5, 7).toInt()
- if (mMonth == 1) {
- mMonth = 0
- } else {
- mMonth -= 1
- }
- mDay = dateString.substring(8).toInt()
- }
- val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday ->
- val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday))
- dateToET!!.setText(input)
- }, mYear, mMonth, mDay)
- mDatePicker.setTitle("Select date")
- mDatePicker.show()
- }
- val adapter: ArrayAdapter = ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList)
- typeSpinner!!.adapter = adapter
- if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?")) {
- val spinnerPosition: Int = adapter.getPosition(activity.args!!.get(activity.args!!.size - 1))
- typeSpinner!!.setSelection(spinnerPosition)
- }
- val submit: Button = rootView.findViewById(R.id.submitFiltered) as Button
- submit.setOnClickListener {
- BuildQuery()
- activity.args = listArgs.toTypedArray()
- FragmentMain.NEW_LOADER = 1
- activity.fragmentManager!!.popBackStack()
- }
- return rootView
- }
-
- private fun BuildQuery() {
- val dateQuery: String = ShiftsEntry.COLUMN_SHIFT_DATE + " BETWEEN ? AND ?"
- val descQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?"
- val typeQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?"
- var dateFrom = "2000-01-01"
- val c: Date = Calendar.getInstance().time
- val df = SimpleDateFormat("yyyy-MM-dd")
- var dateTo: String = df.format(c)
- if (!TextUtils.isEmpty(dateFromET!!.text.toString().trim())) {
- dateFrom = dateFromET!!.text.toString().trim()
- }
- if (!TextUtils.isEmpty(dateToET!!.text.toString().trim())) {
- dateTo = dateToET!!.text.toString().trim()
- }
- activity.selection = dateQuery
- listArgs.add(dateFrom)
- listArgs.add(dateTo)
- if (!TextUtils.isEmpty(LocationET!!.text.toString().trim())) {
- activity.selection = activity.selection + descQuery
- listArgs.add("%" + LocationET!!.text.toString().trim() + "%")
- }
- if (!(typeSpinner!!.selectedItem.toString() == "")) {
- activity.selection = activity.selection + typeQuery
- listArgs.add(typeSpinner!!.selectedItem.toString())
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt
deleted file mode 100644
index eedcee5..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt
+++ /dev/null
@@ -1,507 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.app.DatePickerDialog
-import android.app.DatePickerDialog.OnDateSetListener
-import android.app.TimePickerDialog
-import android.app.TimePickerDialog.OnTimeSetListener
-import android.content.ContentValues
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import android.text.Editable
-import android.text.TextUtils
-import android.text.TextWatcher
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.DatePicker
-import android.widget.EditText
-import android.widget.LinearLayout
-import android.widget.ProgressBar
-import android.widget.RadioButton
-import android.widget.RadioGroup
-import android.widget.ScrollView
-import android.widget.TextView
-import android.widget.TimePicker
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.CursorLoader
-import androidx.loader.content.Loader
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
-import java.util.Calendar
-
-class FragmentAddItem : Fragment(), LoaderManager.LoaderCallbacks {
- lateinit var activity: MainActivity
-
- private var mCurrentProductUri: Uri? = null
- private var mRadioButtonOne: RadioButton? = null
- private var mRadioButtonTwo: RadioButton? = null
- private var mLocationEditText: EditText? = null
- private var mDateEditText: EditText? = null
- private var mDurationTextView: TextView? = null
- private var mTimeInEditText: EditText? = null
- private var mTimeOutEditText: EditText? = null
- private var mBreakEditText: EditText? = null
- private var mUnitEditText: EditText? = null
- private var mPayRateEditText: EditText? = null
- private var mTotalPayTextView: TextView? = null
- private var hourlyDataView: LinearLayout? = null
- private var unitsHolder: LinearLayout? = null
- private var durationHolder: LinearLayout? = null
- private var wholeView: LinearLayout? = null
- private var scrollView: ScrollView? = null
- private var progressBarAI: ProgressBar? = null
- private var mBreaks = 0
- private var mDay = 0
- private var mMonth = 0
- private var mYear = 0
- private var mHoursIn = 0
- private var mMinutesIn = 0
- private var mHoursOut = 0
- private var mMinutesOut = 0
- private var mUnits = 0f
- private var mPayRate = 0f
- private var mType: String? = null
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
- // Inflate the layout for this fragment
- val rootView = inflater.inflate(R.layout.fragment_add_item, container, false)
- setHasOptionsMenu(true)
- activity = (requireActivity() as MainActivity)
-
- progressBarAI = rootView.findViewById(R.id.pd_ai) as ProgressBar
- scrollView = rootView.findViewById(R.id.total_view) as ScrollView
- mRadioGroup = rootView.findViewById(R.id.rg) as RadioGroup
- mRadioButtonOne = rootView.findViewById(R.id.hourly) as RadioButton
- mRadioButtonTwo = rootView.findViewById(R.id.piecerate) as RadioButton
- mLocationEditText = rootView.findViewById(R.id.locationEditText) as EditText
- mDateEditText = rootView.findViewById(R.id.dateEditText) as EditText
- mTimeInEditText = rootView.findViewById(R.id.timeInEditText) as EditText
- mBreakEditText = rootView.findViewById(R.id.breakEditText) as EditText
- mTimeOutEditText = rootView.findViewById(R.id.timeOutEditText) as EditText
- mDurationTextView = rootView.findViewById(R.id.ShiftDuration) as TextView
- mUnitEditText = rootView.findViewById(R.id.unitET) as EditText
- mPayRateEditText = rootView.findViewById(R.id.payrateET) as EditText
- mTotalPayTextView = rootView.findViewById(R.id.totalpayval) as TextView
- hourlyDataView = rootView.findViewById(R.id.hourly_data_holder) as LinearLayout
- unitsHolder = rootView.findViewById(R.id.units_holder) as LinearLayout
- durationHolder = rootView.findViewById(R.id.duration_holder) as LinearLayout
- wholeView = rootView.findViewById(R.id.whole_view) as LinearLayout
- mPayRate = 0.0f
- mUnits = 0.0f
- val b = arguments
- if (b != null) {
- mCurrentProductUri = Uri.parse(b.getString("uri"))
- }
- if (mCurrentProductUri == null) {
- activity.setActionBarTitle(getString(R.string.add_item_title))
- wholeView!!.visibility = View.GONE
- } else {
- activity.setActionBarTitle(getString(R.string.edit_item_title))
- loaderManager.initLoader(EXISTING_PRODUCT_LOADER, null, this)
- }
- mBreakEditText!!.hint = "insert break in minutes"
- mRadioGroup!!.setOnCheckedChangeListener(RadioGroup.OnCheckedChangeListener { radioGroup, i ->
- if (mRadioButtonOne!!.isChecked) {
- mType = mRadioButtonOne!!.text.toString()
- wholeView!!.visibility = View.VISIBLE
- unitsHolder!!.visibility = View.GONE
- hourlyDataView!!.visibility = View.VISIBLE
- durationHolder!!.visibility = View.VISIBLE
- } else if (mRadioButtonTwo!!.isChecked) {
- mType = mRadioButtonTwo!!.text.toString()
- wholeView!!.visibility = View.VISIBLE
- unitsHolder!!.visibility = View.VISIBLE
- hourlyDataView!!.visibility = View.GONE
- durationHolder!!.visibility = View.GONE
- }
- })
- mDateEditText!!.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- //To show current date in the datepicker
- if (TextUtils.isEmpty(mDateEditText!!.text.toString().trim { it <= ' ' })) {
- val mcurrentDate = Calendar.getInstance()
- mYear = mcurrentDate[Calendar.YEAR]
- mMonth = mcurrentDate[Calendar.MONTH]
- mDay = mcurrentDate[Calendar.DAY_OF_MONTH]
- } else {
- val dateString = mDateEditText!!.text.toString().trim { it <= ' ' }
- mYear = dateString.substring(0, 4).toInt()
- mMonth = dateString.substring(5, 7).toInt()
- if (mMonth == 1) {
- mMonth = 0
- } else {
- mMonth = mMonth - 1
- }
- mDay = dateString.substring(8).toInt()
- }
- val mDatePicker = DatePickerDialog((context)!!, object : OnDateSetListener {
- override fun onDateSet(datepicker: DatePicker, selectedyear: Int, selectedmonth: Int, selectedday: Int) {
- var selectedmonth = selectedmonth
- mDateEditText!!.setText(
- (selectedyear.toString() + "-"
- + String.format("%02d", (selectedmonth + 1.also { selectedmonth = it })) + "-"
- + String.format("%02d", selectedday))
- )
- setDate(selectedyear, selectedmonth, selectedday)
- }
- }, mYear, mMonth, mDay)
- mDatePicker.setTitle("Select date")
- mDatePicker.show()
- }
- })
- mTimeInEditText!!.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- if ((mTimeInEditText!!.text.toString() == "")) {
- val mcurrentTime = Calendar.getInstance()
- mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY]
- mMinutesIn = mcurrentTime[Calendar.MINUTE]
- } else {
- mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt()
- mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt()
- }
- val mTimePicker: TimePickerDialog
- mTimePicker = TimePickerDialog(context, object : OnTimeSetListener {
- override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) {
- val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute)
- setTime(selectedMinute, selectedHour)
- mTimeInEditText!!.setText(ddTime)
- }
- }, mHoursIn, mMinutesIn, true) //Yes 24 hour time
- mTimePicker.setTitle("Select Time")
- mTimePicker.show()
- }
- })
- mTimeOutEditText!!.setOnClickListener(object : View.OnClickListener {
- override fun onClick(v: View) {
- if ((mTimeOutEditText!!.text.toString() == "")) {
- val mcurrentTime = Calendar.getInstance()
- mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY]
- mMinutesOut = mcurrentTime[Calendar.MINUTE]
- } else {
- mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt()
- mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt()
- }
- val mTimePicker: TimePickerDialog
- mTimePicker = TimePickerDialog(context, object : OnTimeSetListener {
- override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) {
- val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute)
- setTime2(selectedMinute, selectedHour)
- mTimeOutEditText!!.setText(ddTime)
- }
- }, mHoursOut, mMinutesOut, true) //Yes 24 hour time
- mTimePicker.setTitle("Select Time")
- mTimePicker.show()
- }
- })
- mTimeInEditText!!.addTextChangedListener(object : TextWatcher {
- override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
- override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {}
- override fun afterTextChanged(s: Editable) {
- setDuration()
- calculateTotalPay()
- }
- })
- mTimeOutEditText!!.addTextChangedListener(object : TextWatcher {
- override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
- override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {}
- override fun afterTextChanged(s: Editable) {
- setDuration()
- calculateTotalPay()
- }
- })
- mBreakEditText!!.addTextChangedListener(object : TextWatcher {
- override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
- override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {}
- override fun afterTextChanged(s: Editable) {
- setDuration()
- calculateTotalPay()
- }
- })
- mUnitEditText!!.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
- override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
- override fun afterTextChanged(editable: Editable) {
-// if(mRadioButtonTwo.isChecked()) {
-// mUnits = 0.0f;
-// if (!mUnitEditText.getText().toString().equals("")){
-// mUnits = Float.parseFloat(mUnitEditText.getText().toString());
-// }
-// mPayRate = 0.0f;
-// if (!mPayRateEditText.getText().toString().equals("")){
-// mPayRate = Float.parseFloat(mPayRateEditText.getText().toString());
-// }
-// Float total = mPayRate * mUnits;
-// mTotalPayTextView.setText(String.valueOf(total));
-// }
- calculateTotalPay()
- }
- })
- mPayRateEditText!!.addTextChangedListener(object : TextWatcher {
- override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
- override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
- override fun afterTextChanged(editable: Editable) {
- calculateTotalPay()
- }
- })
- val SubmitProduct = rootView.findViewById(R.id.submit) as Button
- SubmitProduct.setOnClickListener(object : View.OnClickListener {
- override fun onClick(view: View) {
- saveProduct()
- }
- })
- return rootView
- }
-
- private fun calculateTotalPay() {
- var total = 0.0f
- mPayRate = 0.0f
- if (mRadioButtonTwo!!.isChecked) {
- mUnits = 0.0f
- if (mUnitEditText!!.text.toString() != "") {
- mUnits = mUnitEditText!!.text.toString().toFloat()
- }
- if (mPayRateEditText!!.text.toString() != "") {
- mPayRate = mPayRateEditText!!.text.toString().toFloat()
- }
- total = mPayRate * mUnits
- mTotalPayTextView!!.text = total.toString()
- } else if (mRadioButtonOne!!.isChecked) {
- if (mPayRateEditText!!.text.toString() != "") {
- mPayRate = mPayRateEditText!!.text.toString().toFloat()
- total = mPayRate * calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks)
- }
- }
- mTotalPayTextView!!.text = String.format("%.2f", total)
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
- menu.clear()
- }
-
- private fun saveProduct() {
- val typeString = mType
- val descriptionString = mLocationEditText!!.text.toString().trim { it <= ' ' }
- if (TextUtils.isEmpty(descriptionString)) {
- Toast.makeText(context, "please insert Location", Toast.LENGTH_SHORT).show()
- return
- }
- val dateString = mDateEditText!!.text.toString().trim { it <= ' ' }
- if (TextUtils.isEmpty(dateString)) {
- Toast.makeText(context, "please insert Date", Toast.LENGTH_SHORT).show()
- return
- }
- var timeInString: String = ""
- var timeOutString: String = ""
- var breaks = 0
- var units = 0f
- var duration = 0f
- var payRate = 0f
- val payRateString = mPayRateEditText!!.text.toString().trim { it <= ' ' }
- if (!TextUtils.isEmpty(payRateString)) {
- payRate = payRateString.toFloat()
- }
- var totalPay = 0f
- if ((typeString == "Hourly")) {
- timeInString = mTimeInEditText!!.text.toString().trim { it <= ' ' }
- if (TextUtils.isEmpty(timeInString)) {
- Toast.makeText(context, "please insert Time in", Toast.LENGTH_SHORT).show()
- return
- }
- timeOutString = mTimeOutEditText!!.text.toString().trim { it <= ' ' }
- if (TextUtils.isEmpty(timeOutString)) {
- Toast.makeText(context, "please insert Time out", Toast.LENGTH_SHORT).show()
- return
- }
- val breakMins = mBreakEditText!!.text.toString().trim { it <= ' ' }
- if (!TextUtils.isEmpty(breakMins)) {
- breaks = breakMins.toInt()
- if ((breaks.toFloat() / 60) > calculateDurationWithoutBreak(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut)) {
- Toast.makeText(context, "Break larger than duration", Toast.LENGTH_SHORT).show()
- return
- }
- }
- duration = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, breaks)
- totalPay = duration * payRate
- } else if ((typeString == "Piece Rate")) {
- val unitsString = mUnitEditText!!.text.toString().trim { it <= ' ' }
- if (TextUtils.isEmpty(unitsString) || unitsString.toFloat() <= 0) {
- Toast.makeText(context, "Insert Units", Toast.LENGTH_SHORT).show()
- return
- } else {
- units = unitsString.toFloat()
- }
- duration = 0f
- totalPay = units * payRate
- }
- val values = ContentValues()
- values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeString)
- values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionString)
- values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateString)
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInString)
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutString)
- values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, duration)
- values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breaks)
- values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, units)
- values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payRate)
- values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay)
- if (mCurrentProductUri == null) {
- val newUri = activity.contentResolver.insert(ShiftsEntry.CONTENT_URI, values)
- if (newUri == null) {
- Toast.makeText(context, getString(R.string.insert_item_failed),
- Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, getString(R.string.insert_item_successful),
- Toast.LENGTH_SHORT).show()
- }
- } else {
- val rowsAffected = activity.contentResolver.update(mCurrentProductUri!!, values, null, null)
- if (rowsAffected == 0) {
- Toast.makeText(context, getString(R.string.update_item_failed),
- Toast.LENGTH_SHORT).show()
- } else {
- Toast.makeText(context, getString(R.string.update_item_successful),
- Toast.LENGTH_SHORT).show()
- }
- }
- (activity.fragmentManager)!!.popBackStack("main", 0)
- }
-
- private fun setDuration() {
- val mcurrentTime = Calendar.getInstance()
- mBreaks = 0
- if (mBreakEditText!!.text.toString() != "") {
- mBreaks = mBreakEditText!!.text.toString().toInt()
- }
- if ((mTimeOutEditText!!.text.toString() == "")) {
- mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY]
- mMinutesOut = mcurrentTime[Calendar.MINUTE]
- } else {
- mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt()
- mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt()
- }
- if ((mTimeInEditText!!.text.toString() == "")) {
- mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY]
- mMinutesIn = mcurrentTime[Calendar.MINUTE]
- } else {
- mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt()
- mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt()
- }
- mDurationTextView!!.text = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks).toString() + " hours"
- }
-
- private fun setDate(year: Int, month: Int, day: Int) {
- mYear = year
- mMonth = month
- mDay = day
- }
-
- private fun setTime(minutes: Int, hours: Int) {
- mMinutesIn = minutes
- mHoursIn = hours
- }
-
- private fun setTime2(minutes: Int, hours: Int) {
- mMinutesOut = minutes
- mHoursOut = hours
- }
-
- private fun calculateDuration(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int, breaks: Int): Float {
- val duration: Float
- if (hoursOut > hoursIn) {
- duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60)
- } else {
- duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60) + 24)
- }
- val s = String.format("%.2f", duration)
- return s.toFloat()
- }
-
- private fun calculateDurationWithoutBreak(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int): Float {
- val duration: Float
- if (hoursOut > hoursIn) {
- duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60)))
- } else {
- duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24)
- }
- val s = String.format("%.2f", duration)
- return s.toFloat()
- }
-
- override fun onCreateLoader(id: Int, args: Bundle?): Loader {
- progressBarAI!!.visibility = View.VISIBLE
- scrollView!!.visibility = View.GONE
- val projection = arrayOf(
- ShiftsEntry._ID,
- ShiftsEntry.COLUMN_SHIFT_DESCRIPTION,
- ShiftsEntry.COLUMN_SHIFT_DATE,
- ShiftsEntry.COLUMN_SHIFT_TIME_IN,
- ShiftsEntry.COLUMN_SHIFT_TIME_OUT,
- ShiftsEntry.COLUMN_SHIFT_BREAK,
- ShiftsEntry.COLUMN_SHIFT_DURATION,
- ShiftsEntry.COLUMN_SHIFT_TYPE,
- ShiftsEntry.COLUMN_SHIFT_PAYRATE,
- ShiftsEntry.COLUMN_SHIFT_UNIT,
- ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- return CursorLoader((context)!!, (mCurrentProductUri)!!,
- projection, null, null, null)
- }
-
- override fun onLoaderReset(loader: Loader) {}
-
- override fun onLoadFinished(loader: Loader, cursor: Cursor?) {
- progressBarAI!!.visibility = View.GONE
- scrollView!!.visibility = View.VISIBLE
- if (cursor == null || cursor.count < 1) {
- return
- }
- if (cursor.moveToFirst()) {
- val descriptionColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
- val dateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE)
- val timeInColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
- val timeOutColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
- val breakColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK)
- val durationColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION)
- val typeColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE)
- val unitColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT)
- val payrateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE)
- val totalPayColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- val type = cursor.getString(typeColumnIndex)
- val description = cursor.getString(descriptionColumnIndex)
- val date = cursor.getString(dateColumnIndex)
- val timeIn = cursor.getString(timeInColumnIndex)
- val timeOut = cursor.getString(timeOutColumnIndex)
- val breaks = cursor.getInt(breakColumnIndex)
- val duration = cursor.getFloat(durationColumnIndex)
- val unit = cursor.getFloat(unitColumnIndex)
- val payrate = cursor.getFloat(payrateColumnIndex)
- val totalPay = cursor.getFloat(totalPayColumnIndex)
- mLocationEditText!!.setText(description)
- mDateEditText!!.setText(date)
- if ((type == "Hourly") || (type == "hourly")) {
- mRadioButtonOne!!.isChecked = true
- mRadioButtonTwo!!.isChecked = false
- mTimeInEditText!!.setText(timeIn)
- mTimeOutEditText!!.setText(timeOut)
- mBreakEditText!!.setText(Integer.toString(breaks))
- mDurationTextView!!.text = String.format("%.2f", duration) + " Hours"
- } else if ((type == "Piece Rate")) {
- mRadioButtonOne!!.isChecked = false
- mRadioButtonTwo!!.isChecked = true
- mUnitEditText!!.setText(java.lang.Float.toString(unit))
- }
- mPayRateEditText!!.setText(String.format("%.2f", payrate))
- mTotalPayTextView!!.text = String.format("%.2f", totalPay)
- }
- }
-
- companion object {
- private val EXISTING_PRODUCT_LOADER = 0
- var mRadioGroup: RadioGroup? = null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt
deleted file mode 100644
index 2b09b09..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt
+++ /dev/null
@@ -1,459 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.app.Activity
-import android.app.AlertDialog
-import android.content.ContentValues
-import android.content.DialogInterface
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment
-import android.os.StrictMode
-import android.os.StrictMode.VmPolicy
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ListView
-import android.widget.Toast
-import androidx.core.app.ActivityCompat
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentTransaction
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.CursorLoader
-import androidx.loader.content.Loader
-import com.ajts.androidmads.library.SQLiteToExcel
-import com.ajts.androidmads.library.SQLiteToExcel.ExportListener
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
-import com.appttude.h_mal.farmr.data.ShiftsDbHelper
-import com.google.android.material.floatingactionbutton.FloatingActionButton
-import java.io.File
-
-class FragmentMain : Fragment(), LoaderManager.LoaderCallbacks {
- var mCursorAdapter: ShiftsCursorAdapter? = null
- var shiftsDbhelper: ShiftsDbHelper? = null
- lateinit var defaultLoaderCallback: LoaderManager.LoaderCallbacks
- lateinit var activity: MainActivity
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View? {
- // Inflate the layout for this fragment
- val rootView = inflater.inflate(R.layout.fragment_main, container, false)
- setHasOptionsMenu(true)
- activity = (requireActivity() as MainActivity)
-
- activity.setActionBarTitle(getString(R.string.app_name))
- activity.filter = activity.getSharedPreferences("PREFS", 0)
- activity.sortOrder = activity.filter?.getString("Filter", null)
- defaultLoaderCallback = this
- val productListView = rootView.findViewById(R.id.list_item_view) as ListView
- val emptyView = rootView.findViewById(R.id.empty_view)
- productListView.emptyView = emptyView
- mCursorAdapter = ShiftsCursorAdapter(activity, null)
- productListView.adapter = mCursorAdapter
- loaderManager.initLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
- val fab = rootView.findViewById(R.id.fab1)
- fab.setOnClickListener {
- val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction()
- fragmentTransaction.replace(R.id.container, FragmentAddItem()).addToBackStack("additem").commit()
- }
- return rootView
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.delete_all -> {
- deleteAllProducts()
- return true
- }
-
- R.id.help -> {
- AlertDialog.Builder(context)
- .setTitle("Help & Support:")
- .setView(R.layout.dialog_layout)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show()
- return true
- }
-
- R.id.filter_data -> {
- val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction()
- fragmentTransaction.replace(R.id.container, FilterDataFragment()).addToBackStack("filterdata").commit()
- return true
- }
-
- R.id.sort_data -> {
- sortData()
- return true
- }
-
- R.id.clear_filter -> {
- activity.args = null
- activity.selection = null
- NEW_LOADER = 0
- loaderManager.restartLoader(DEFAULT_LOADER, null, this)
- return true
- }
-
- R.id.export_data -> {
- if (checkStoragePermissions(activity)) {
- AlertDialog.Builder(context)
- .setTitle("Export?")
- .setMessage("Exporting current filtered data. Continue?")
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show()
- } else {
- Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT).show()
- }
- return true
- }
-
- R.id.action_favorite -> {
- AlertDialog.Builder(context)
- .setTitle("Info:")
- .setMessage(retrieveInfo())
- .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show()
- return true
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun sortData() {
- val grpname = arrayOf("Added", "Date", "Name")
- val sortQuery = arrayOf("")
- var checkedItem = -1
- if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry._ID)) {
- checkedItem = 0
- sortQuery[0] = ShiftsEntry._ID
- } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DATE)) {
- checkedItem = 1
- sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE
- } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) {
- checkedItem = 2
- sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
- }
- val alt_bld = AlertDialog.Builder(context)
- //alt_bld.setIcon(R.drawable.icon);
- alt_bld.setTitle("Sort by:")
- alt_bld.setSingleChoiceItems(grpname, checkedItem, DialogInterface.OnClickListener { dialog, item ->
- when (item) {
- 0 -> {
- sortQuery[0] = ShiftsEntry._ID
- return@OnClickListener
- }
-
- 1 -> {
- sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE
- return@OnClickListener
- }
-
- 2 -> sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
- }
- }).setPositiveButton("Ascending") { dialog, id ->
- activity.sortOrder = sortQuery[0] + " ASC"
- activity.filter!!.edit().putString("Filter", activity.sortOrder).apply()
- loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
- dialog.dismiss()
- }.setNegativeButton("Descending") { dialog, id ->
- activity.sortOrder = sortQuery[0] + " DESC"
- activity.filter!!.edit().putString("Filter", activity.sortOrder).apply()
- loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
- dialog.dismiss()
- }
- val alert = alt_bld.create()
- alert.show()
- }
-
- private fun deleteAllProducts() {
- AlertDialog.Builder(context)
- .setTitle("Delete?")
- .setMessage("Are you sure you want to delete all date?")
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 ->
- val rowsDeleted = activity.contentResolver.delete(ShiftsEntry.CONTENT_URI, null, null)
- Toast.makeText(context, "$rowsDeleted Items Deleted", Toast.LENGTH_SHORT).show()
- }.create().show()
- }
-
- override fun onResume() {
- super.onResume()
- if (NEW_LOADER > DEFAULT_LOADER) {
- loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
- println("reloading loader")
- }
- }
-
- private fun ExportData() {
- val permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
- if (permission != PackageManager.PERMISSION_GRANTED) {
- Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show()
- return
- }
- shiftsDbhelper = ShiftsDbHelper(context)
- val database = shiftsDbhelper!!.writableDatabase
- val projection_export = arrayOf(
- ShiftsEntry.COLUMN_SHIFT_DESCRIPTION,
- ShiftsEntry.COLUMN_SHIFT_DATE,
- ShiftsEntry.COLUMN_SHIFT_TIME_IN,
- ShiftsEntry.COLUMN_SHIFT_TIME_OUT,
- ShiftsEntry.COLUMN_SHIFT_BREAK,
- ShiftsEntry.COLUMN_SHIFT_DURATION,
- ShiftsEntry.COLUMN_SHIFT_TYPE,
- ShiftsEntry.COLUMN_SHIFT_UNIT,
- ShiftsEntry.COLUMN_SHIFT_PAYRATE,
- ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- val cursor = activity.contentResolver.query(
- ShiftsEntry.CONTENT_URI,
- projection_export,
- activity.selection,
- activity.args,
- activity.sortOrder)
- database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null)
- var totalDuration = 0.00f
- var totalUnits = 0.00f
- var totalPay = 0.00f
- try {
- while (cursor!!.moveToNext()) {
- val descriptionColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION))
- val dateColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE))
- val timeInColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN))
- val timeOutColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT))
- val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION))
- val breakOutColumnIndex = cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK))
- val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE))
- val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT))
- val payrateColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE))
- val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY))
- totalUnits = totalUnits + unitColumnIndex
- totalDuration = totalDuration + durationColumnIndex
- totalPay = totalPay + totalpayColumnIndex
- val values = ContentValues()
- values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex)
- values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex)
- database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values)
- }
- } catch (e: Exception) {
- Log.e("FragmentMain", "ExportData: ", e)
- } finally {
- val values = ContentValues()
- values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "")
- values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "")
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "")
- values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "")
- values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:")
- values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration)
- values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:")
- values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits)
- values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:")
- values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay)
- database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values)
- cursor!!.close()
- }
- val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp"
- val file = File(savePath)
- if (!file.exists()) {
- file.mkdirs()
- }
- val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath)
- sqLiteToExcel.exportSingleTable("shiftsexport", "shifthistory.xls", object : ExportListener {
- override fun onStart() {}
- override fun onCompleted(filePath: String) {
- Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show()
- val newPath = Uri.parse("file://$savePath/shifthistory.xls")
- val builder = VmPolicy.Builder()
- StrictMode.setVmPolicy(builder.build())
- val emailintent = Intent(Intent.ACTION_SEND)
- emailintent.type = "application/vnd.ms-excel"
- emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts")
- emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.")
- emailintent.putExtra(Intent.EXTRA_STREAM, newPath)
- startActivity(Intent.createChooser(emailintent, "Send Email"))
- }
-
- override fun onError(e: Exception) {
- println("Error msg: $e")
- Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show()
- }
- })
- }
-
- private fun retrieveInfo(): String {
- val projection = arrayOf(
- ShiftsEntry._ID,
- ShiftsEntry.COLUMN_SHIFT_DESCRIPTION,
- ShiftsEntry.COLUMN_SHIFT_DATE,
- ShiftsEntry.COLUMN_SHIFT_TIME_IN,
- ShiftsEntry.COLUMN_SHIFT_TIME_OUT,
- ShiftsEntry.COLUMN_SHIFT_BREAK,
- ShiftsEntry.COLUMN_SHIFT_DURATION,
- ShiftsEntry.COLUMN_SHIFT_TYPE,
- ShiftsEntry.COLUMN_SHIFT_UNIT,
- ShiftsEntry.COLUMN_SHIFT_PAYRATE,
- ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- val cursor = activity.contentResolver.query(
- ShiftsEntry.CONTENT_URI,
- projection,
- activity.selection,
- activity.args,
- activity.sortOrder)
- var totalDuration = 0.0f
- var countOfTypeH = 0
- var countOfTypeP = 0
- var totalUnits = 0f
- var totalPay = 0f
- var lines = 0
- try {
- while (cursor!!.moveToNext()) {
- val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION))
- val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE))
- val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT))
- val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY))
- totalDuration = totalDuration + durationColumnIndex
- if (typeColumnIndex.contains("Hourly")) {
- countOfTypeH = countOfTypeH + 1
- } else if (typeColumnIndex.contains("Piece")) {
- countOfTypeP = countOfTypeP + 1
- }
- totalUnits = totalUnits + unitColumnIndex
- totalPay = totalPay + totalpayColumnIndex
- }
- } finally {
- if (cursor != null && cursor.count > 0) {
- lines = cursor.count
- cursor.close()
- }
- }
- return buildInfoString(totalDuration, countOfTypeH, countOfTypeP, totalUnits, totalPay, lines)
- }
-
- @SuppressLint("DefaultLocale")
- fun buildInfoString(totalDuration: Float, countOfTypeH: Int, countOfTypeP: Int, totalUnits: Float, totalPay: Float, lines: Int): String {
- var textString: String
- textString = "$lines Shifts"
- if (countOfTypeH != 0 && countOfTypeP != 0) {
- textString = "$textString ($countOfTypeH Hourly/$countOfTypeP Piece Rate)"
- }
- if (countOfTypeH != 0) {
- textString = """
- $textString
- Total Hours: ${String.format("%.2f", totalDuration)}
- """.trimIndent()
- }
- if (countOfTypeP != 0) {
- textString = """
- $textString
- Total Units: ${String.format("%.2f", totalUnits)}
- """.trimIndent()
- }
- if (totalPay != 0f) {
- textString = """
- $textString
- Total Pay: ${"$"}${String.format("%.2f", totalPay)}
- """.trimIndent()
- }
- return textString
- }
-
- override fun onCreateLoader(i: Int, bundle: Bundle?): Loader {
- val projection = arrayOf(
- ShiftsEntry._ID,
- ShiftsEntry.COLUMN_SHIFT_DESCRIPTION,
- ShiftsEntry.COLUMN_SHIFT_DATE,
- ShiftsEntry.COLUMN_SHIFT_TIME_IN,
- ShiftsEntry.COLUMN_SHIFT_TIME_OUT,
- ShiftsEntry.COLUMN_SHIFT_BREAK,
- ShiftsEntry.COLUMN_SHIFT_DURATION,
- ShiftsEntry.COLUMN_SHIFT_TYPE,
- ShiftsEntry.COLUMN_SHIFT_PAYRATE,
- ShiftsEntry.COLUMN_SHIFT_UNIT,
- ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- return CursorLoader(context!!,
- ShiftsEntry.CONTENT_URI,
- projection,
- activity.selection,
- activity.args,
- activity.sortOrder)
- }
-
- override fun onLoadFinished(loader: Loader, cursor: Cursor) {
- mCursorAdapter!!.swapCursor(cursor)
- }
-
- override fun onLoaderReset(loader: Loader) {
- mCursorAdapter!!.swapCursor(null)
- }
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- println("request code$requestCode")
- if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
- if (grantResults.size > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- exportDialog()
- } else {
- Toast.makeText(context, "Storage Permissions denied", Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- fun exportDialog() {
- AlertDialog.Builder(context)
- .setTitle("Export?")
- .setMessage("Exporting current filtered data. Continue?")
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show()
- }
-
- companion object {
- const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
- private const val DEFAULT_LOADER = 0
- var NEW_LOADER = 0
-
- // // Storage Permissions
- // private static final int REQUEST_EXTERNAL_STORAGE = 1;
- // private static String[] PERMISSIONS_STORAGE = {
- // Manifest.permission.READ_EXTERNAL_STORAGE,
- // Manifest.permission.WRITE_EXTERNAL_STORAGE
- // };
- // /**
- // * Checks if the app has permission to write to device storage
- // *
- // * If the app does not has permission then the user will be prompted to grant permissions
- // *
- // * @param activity
- // */
- // public static void verifyStoragePermissions(Activity activity) {
- // // Check if we have write permission
- // int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
- //
- // if (permission != PackageManager.PERMISSION_GRANTED) {
- // // We don't have permission so prompt the user
- // ActivityCompat.requestPermissions(
- // activity,
- // PERMISSIONS_STORAGE,
- // REQUEST_EXTERNAL_STORAGE
- // );
- // }
- // }
- fun checkStoragePermissions(activity: Activity?): Boolean {
- var status = false
- val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE)
- if (permission == PackageManager.PERMISSION_GRANTED) {
- status = true
- }
- return status
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt
deleted file mode 100644
index eedc41d..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.ProgressBar
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentTransaction
-import androidx.loader.app.LoaderManager
-import androidx.loader.content.CursorLoader
-import androidx.loader.content.Loader
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
-
-class FurtherInfoFragment : Fragment(), LoaderManager.LoaderCallbacks {
- private var typeTV: TextView? = null
- private var descriptionTV: TextView? = null
- private var dateTV: TextView? = null
- private var times: TextView? = null
- private var breakTV: TextView? = null
- private var durationTV: TextView? = null
- private var unitsTV: TextView? = null
- private var payRateTV: TextView? = null
- private var totalPayTV: TextView? = null
- private var hourlyDetailHolder: LinearLayout? = null
- private var unitsHolder: LinearLayout? = null
- private var wholeView: LinearLayout? = null
- private var progressBarFI: ProgressBar? = null
- private var editButton: Button? = null
- private var CurrentUri: Uri? = null
-
- lateinit var activity: MainActivity
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?): View {
- // Inflate the layout for this fragment
- val rootView: View = inflater.inflate(R.layout.fragment_futher_info, container, false)
- setHasOptionsMenu(true)
- activity = (requireActivity() as MainActivity)
- activity.setActionBarTitle(getString(R.string.further_info_title))
-
- progressBarFI = rootView.findViewById(R.id.progressBar_info) as ProgressBar?
- wholeView = rootView.findViewById(R.id.further_info_view) as LinearLayout?
- typeTV = rootView.findViewById(R.id.details_shift) as TextView?
- descriptionTV = rootView.findViewById(R.id.details_desc) as TextView?
- dateTV = rootView.findViewById(R.id.details_date) as TextView?
- times = rootView.findViewById(R.id.details_time) as TextView?
- breakTV = rootView.findViewById(R.id.details_breaks) as TextView?
- durationTV = rootView.findViewById(R.id.details_duration) as TextView?
- unitsTV = rootView.findViewById(R.id.details_units) as TextView?
- payRateTV = rootView.findViewById(R.id.details_pay_rate) as TextView?
- totalPayTV = rootView.findViewById(R.id.details_totalpay) as TextView?
- editButton = rootView.findViewById(R.id.details_edit) as Button?
- hourlyDetailHolder = rootView.findViewById(R.id.details_hourly_details) as LinearLayout?
- unitsHolder = rootView.findViewById(R.id.details_units_holder) as LinearLayout?
- val b: Bundle? = arguments
- CurrentUri = Uri.parse(b!!.getString("uri"))
- loaderManager.initLoader(DEFAULT_LOADER, null, this)
- editButton!!.setOnClickListener(object : View.OnClickListener {
- override fun onClick(view: View) {
- val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction()
- val fragment: Fragment = FragmentAddItem()
- fragment.arguments = b
- fragmentTransaction.replace(R.id.container, fragment).addToBackStack("additem").commit()
- }
- })
- return rootView
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
- menu.clear()
- }
-
- override fun onCreateLoader(id: Int, args: Bundle?): Loader {
- progressBarFI!!.visibility = View.VISIBLE
- wholeView!!.visibility = View.GONE
- val projection: Array = arrayOf(
- ShiftsEntry._ID,
- ShiftsEntry.COLUMN_SHIFT_DESCRIPTION,
- ShiftsEntry.COLUMN_SHIFT_DATE,
- ShiftsEntry.COLUMN_SHIFT_TIME_IN,
- ShiftsEntry.COLUMN_SHIFT_TIME_OUT,
- ShiftsEntry.COLUMN_SHIFT_BREAK,
- ShiftsEntry.COLUMN_SHIFT_DURATION,
- ShiftsEntry.COLUMN_SHIFT_TYPE,
- ShiftsEntry.COLUMN_SHIFT_PAYRATE,
- ShiftsEntry.COLUMN_SHIFT_UNIT,
- ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- return CursorLoader((context)!!, (CurrentUri)!!,
- projection, null, null, null)
- }
-
- override fun onLoadFinished(loader: Loader, cursor: Cursor) {
- progressBarFI!!.visibility = View.GONE
- wholeView!!.visibility = View.VISIBLE
- if (cursor == null || cursor.count < 1) {
- return
- }
- if (cursor.moveToFirst()) {
- val descriptionColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
- val dateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE)
- val timeInColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
- val timeOutColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
- val breakColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK)
- val durationColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION)
- val typeColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE)
- val unitColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT)
- val payrateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE)
- val totalPayColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)
- val type: String = cursor.getString(typeColumnIndex)
- val description: String = cursor.getString(descriptionColumnIndex)
- val date: String = cursor.getString(dateColumnIndex)
- val timeIn: String = cursor.getString(timeInColumnIndex)
- val timeOut: String = cursor.getString(timeOutColumnIndex)
- val breaks: Int = cursor.getInt(breakColumnIndex)
- val duration: Float = cursor.getFloat(durationColumnIndex)
- val unit: Float = cursor.getFloat(unitColumnIndex)
- val payrate: Float = cursor.getFloat(payrateColumnIndex)
- val totalPay: Float = cursor.getFloat(totalPayColumnIndex)
- var durationString: String = ShiftsCursorAdapter.Companion.timeValues(duration).get(0) + " Hours " + ShiftsCursorAdapter.Companion.timeValues(duration).get(1) + " Minutes "
- if (breaks != 0) {
- durationString = durationString + " (+ " + Integer.toString(breaks) + " minutes break)"
- }
- typeTV!!.text = type
- descriptionTV!!.text = description
- dateTV!!.text = date
- var totalPaid: String? = ""
- val currency: String = "$"
- if ((type == "Hourly")) {
- hourlyDetailHolder!!.visibility = View.VISIBLE
- unitsHolder!!.visibility = View.GONE
- times!!.text = timeIn + " - " + timeOut
- breakTV!!.text = Integer.toString(breaks) + "mins"
- durationTV!!.text = durationString
- totalPaid = (String.format("%.2f", duration) + " Hours @ " + currency + String.format("%.2f", payrate) + " per Hour" + "\n"
- + "Equals: " + currency + String.format("%.2f", totalPay))
- } else if ((type == "Piece Rate")) {
- hourlyDetailHolder!!.visibility = View.GONE
- unitsHolder!!.visibility = View.VISIBLE
- unitsTV!!.text = String.format("%.2f", unit)
- totalPaid = (String.format("%.2f", unit) + " Units @ " + currency + String.format("%.2f", payrate) + " per Unit" + "\n"
- + "Equals: " + currency + String.format("%.2f", totalPay))
- }
- payRateTV!!.text = String.format("%.2f", payrate)
- totalPayTV!!.text = totalPaid
- }
- }
-
- override fun onLoaderReset(loader: Loader) {}
-
- companion object {
- private val DEFAULT_LOADER: Int = 0
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt
deleted file mode 100644
index a0cbd9c..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.Manifest
-import android.app.Activity
-import android.app.AlertDialog
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.os.Bundle
-import android.view.Menu
-import android.view.View
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.Toolbar
-import androidx.core.app.ActivityCompat
-import androidx.fragment.app.FragmentManager
-
-class MainActivity : AppCompatActivity() {
- var filter: SharedPreferences? = null
- var context: Context? = null
- var sortOrder: String? = null
- var selection: String? = null
- var args: Array? = null
- private var toolbar: Toolbar? = null
- var fragmentManager: FragmentManager? = null
- private var currentFragment: String? = null
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.main_view)
- verifyStoragePermissions(this)
- toolbar = findViewById(R.id.toolbar) as Toolbar
- setSupportActionBar(toolbar)
- fragmentManager = supportFragmentManager
- val fragmentTransaction = fragmentManager?.beginTransaction()
- fragmentTransaction?.replace(R.id.container, FragmentMain())?.addToBackStack("main")?.commit()
- fragmentManager?.addOnBackStackChangedListener {
- val f = fragmentManager?.fragments
- val frag = f?.get(0)
- currentFragment = frag?.javaClass?.simpleName
- }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- // Inflate the menu; this adds items to the action bar if it is present.
- menuInflater.inflate(R.menu.menu_main, menu)
- return true
- }
-
- override fun onBackPressed() {
- when (currentFragment) {
- "FragmentMain" -> {
- AlertDialog.Builder(this)
- .setTitle("Leave?")
- .setMessage("Are you sure you want to exit Farmr?")
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 ->
- val intent = Intent(Intent.ACTION_MAIN)
- intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
- intent.addCategory(Intent.CATEGORY_HOME)
- startActivity(intent)
- finish()
- System.exit(0)
- }.create().show()
- return
- }
-
- "FragmentAddItem" -> {
- if (FragmentAddItem.Companion.mRadioGroup!!.checkedRadioButtonId == -1) {
- fragmentManager!!.popBackStack()
- } else {
- AlertDialog.Builder(this)
- .setTitle("Discard Changes?")
- .setMessage("Are you sure you want to discard changes?")
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes) { arg0, arg1 -> fragmentManager!!.popBackStack() }.create().show()
- }
- return
- }
-
- else -> if (fragmentManager!!.backStackEntryCount > 1) {
- fragmentManager!!.popBackStack()
- }
- }
- }
-
- fun setActionBarTitle(title: String?) {
- toolbar!!.title = title
- }
-
- // Storage Permissions
- private val REQUEST_EXTERNAL_STORAGE = 1
- private val PERMISSIONS_STORAGE = arrayOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- )
-
- /**
- * Checks if the app has permission to write to device storage
- *
- * If the app does not has permission then the user will be prompted to grant permissions
- *
- * @param activity
- */
- fun verifyStoragePermissions(activity: Activity?) {
- // Check if we have write permission
- val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE)
- if (permission != PackageManager.PERMISSION_GRANTED) {
- // We don't have permission so prompt the user
- ActivityCompat.requestPermissions(
- activity,
- PERMISSIONS_STORAGE,
- REQUEST_EXTERNAL_STORAGE
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt
deleted file mode 100644
index 0163f95..0000000
--- a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.appttude.h_mal.farmr
-
-import android.app.AlertDialog
-import android.content.ContentUris
-import android.content.Context
-import android.content.DialogInterface
-import android.database.Cursor
-import android.net.Uri
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.View.OnLongClickListener
-import android.view.ViewGroup
-import android.widget.CursorAdapter
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.fragment.app.FragmentTransaction
-import com.appttude.h_mal.farmr.data.ShiftProvider
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
-import kotlin.math.floor
-
-/**
- * Created by h_mal on 26/12/2017.
- */
-class ShiftsCursorAdapter constructor(private val activity: MainActivity, c: Cursor?) : CursorAdapter(activity, c, 0) {
- private var mContext: Context? = null
- var shiftProvider: ShiftProvider? = null
- override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
- return LayoutInflater.from(context).inflate(R.layout.list_item_1, parent, false)
- }
-
- override fun bindView(view: View, context: Context, cursor: Cursor) {
- mContext = context
- val descriptionTextView: TextView = view.findViewById(R.id.location) as TextView
- val dateTextView: TextView = view.findViewById(R.id.date) as TextView
- val totalPay: TextView = view.findViewById(R.id.total_pay) as TextView
- val hoursView: TextView = view.findViewById(R.id.hours) as TextView
- val h: TextView = view.findViewById(R.id.h) as TextView
- val minutesView: TextView = view.findViewById(R.id.minutes) as TextView
- val m: TextView = view.findViewById(R.id.m) as TextView
- val editView: ImageView = view.findViewById(R.id.imageView) as ImageView
- h.text = "h"
- m.text = "m"
- val typeColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE))
- val descriptionColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION))
- val dateColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE))
- val durationColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION))
- val unitsColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT))
- val totalpayColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY))
- descriptionTextView.text = descriptionColumnIndex
- dateTextView.text = newDate(dateColumnIndex)
- totalPay.text = String.format("%.2f", totalpayColumnIndex)
- if ((typeColumnIndex == "Piece Rate") && durationColumnIndex == 0f) {
- hoursView.text = unitsColumnIndex.toString()
- h.text = ""
- minutesView.text = ""
- m.text = "pcs"
- } else // if(typeColumnIndex.equals("Hourly") || typeColumnIndex.equals("hourly"))
- {
- hoursView.text = timeValues(durationColumnIndex).get(0)
- minutesView.text = timeValues(durationColumnIndex).get(1)
- }
- val ID: Long = cursor.getLong(cursor.getColumnIndexOrThrow(ShiftsEntry._ID))
- val currentProductUri: Uri = ContentUris.withAppendedId(ShiftsEntry.CONTENT_URI, ID)
- val b: Bundle = Bundle()
- b.putString("uri", currentProductUri.toString())
- view.setOnClickListener { // activity.clickOnViewItem(ID);
- val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction()
- val fragment2: FurtherInfoFragment = FurtherInfoFragment()
- fragment2.arguments = b
- fragmentTransaction.replace(R.id.container, fragment2).addToBackStack("furtherinfo").commit()
- }
- editView.setOnClickListener {
- val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction()
- val fragment3: FragmentAddItem = FragmentAddItem()
- fragment3.arguments = b
- fragmentTransaction.replace(R.id.container, fragment3).addToBackStack("additem").commit()
- }
- view.setOnLongClickListener {
- println("long click: $ID")
- val builder: AlertDialog.Builder = AlertDialog.Builder(mContext)
- builder.setMessage("Are you sure you want to delete")
- builder.setPositiveButton("delete") { dialog, id -> deleteProduct(ID) }
- builder.setNegativeButton("cancel") { dialog, id ->
- dialog?.dismiss()
- }
- val alertDialog: AlertDialog = builder.create()
- alertDialog.show()
- true
- }
- }
-
- private fun deleteProduct(id: Long) {
- val args: Array = arrayOf(id.toString())
- // String whereClause = String.format(ShiftsEntry._ID + " in (%s)", new Object[] { TextUtils.join(",", Collections.nCopies(args.length, "?")) }); //for deleting multiple lines
- mContext!!.contentResolver.delete(ShiftsEntry.CONTENT_URI, ShiftsEntry._ID + "=?", args)
- }
-
- private fun newDate(dateString: String): String {
- var returnString: String? = "01/01/2010"
- val year: String = dateString.substring(0, 4)
- val month: String = dateString.substring(5, 7)
- val day: String = dateString.substring(8)
- returnString = "$day-$month-$year"
- return returnString
- }
-
- companion object {
- fun timeValues(duration: Float): Array {
- val hours: Int = floor(duration.toDouble()).toInt()
- val minutes: Int = ((duration - hours) * 60).toInt()
- val hoursString: String = hours.toString() + ""
- val minutesString: String = String.format("%02d", minutes)
- return arrayOf(hoursString, minutesString)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt
new file mode 100644
index 0000000..3ade301
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt
@@ -0,0 +1,5 @@
+package com.appttude.h_mal.farmr.base
+
+interface BackPressedListener {
+ fun onBackPressed(): Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt
new file mode 100644
index 0000000..6ddc382
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt
@@ -0,0 +1,42 @@
+package com.appttude.h_mal.farmr.base
+
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import com.appttude.h_mal.farmr.utils.displayToast
+
+abstract class BaseActivity : AppCompatActivity() {
+
+
+ /**
+ * Creates a loading view which to be shown during async operations
+ *
+ * #setOnClickListener(null) is an ugly work around to prevent under being clicked during
+ * loading
+ */
+
+ fun startActivity(activity: Class) {
+ val intent = Intent(this, activity)
+ startActivity(intent)
+ }
+
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onStarted() {}
+
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onSuccess(data: Any?) {}
+
+ /**
+ * Called in case of failure or some error emitted from the liveData in viewModel
+ */
+ open fun onFailure(error: Any?) {
+ if (error is String) displayToast(error)
+ }
+
+ fun setTitleInActionBar(title: String) {
+ supportActionBar?.title = title
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt
new file mode 100644
index 0000000..055e5f3
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt
@@ -0,0 +1,31 @@
+package com.appttude.h_mal.farmr.base
+
+import android.app.Application
+import com.appttude.h_mal.farmr.data.RepositoryImpl
+import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
+import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
+import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
+import org.kodein.di.Kodein
+import org.kodein.di.KodeinAware
+import org.kodein.di.android.x.androidXModule
+import org.kodein.di.generic.bind
+import org.kodein.di.generic.instance
+import org.kodein.di.generic.provider
+import org.kodein.di.generic.singleton
+
+abstract class BaseApplication() : Application(), KodeinAware {
+
+ // Kodein creation of modules to be retrieve within the app
+ override val kodein = Kodein.lazy {
+ import(androidXModule(this@BaseApplication))
+
+ bind() from singleton { createDatabase() }
+ bind() from singleton { createPrefs() }
+ bind() from singleton { RepositoryImpl(instance(), instance()) }
+
+ bind() from provider { ApplicationViewModelFactory(instance()) }
+ }
+
+ abstract fun createDatabase(): LegacyDatabase
+ abstract fun createPrefs(): PreferenceProvider
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt
new file mode 100644
index 0000000..a74edba
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt
@@ -0,0 +1,81 @@
+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.ViewModelLazy
+import com.appttude.h_mal.farmr.model.ViewState
+import com.appttude.h_mal.farmr.utils.getGenericClassAt
+import com.appttude.h_mal.farmr.utils.popBackStack
+import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
+import org.kodein.di.KodeinAware
+import org.kodein.di.android.x.kodein
+import org.kodein.di.generic.instance
+import kotlin.properties.Delegates
+
+@Suppress("EmptyMethod", "EmptyMethod")
+abstract class BaseFragment(@LayoutRes contentLayoutId: Int) :
+ Fragment(contentLayoutId), KodeinAware {
+
+ override val kodein by kodein()
+ private val factory by instance()
+
+ val viewModel: V by getViewModel()
+
+ private fun getViewModel(): Lazy =
+ ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
+ factoryProducer = { factory } )
+
+ var mActivity: BaseActivity? = null
+
+ private var shortAnimationDuration by Delegates.notNull()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ mActivity = requireActivity() as BaseActivity
+ configureObserver()
+ }
+
+ private fun configureObserver() {
+ viewModel.uiState.observe(viewLifecycleOwner) {
+ when (it) {
+ is ViewState.HasStarted -> onStarted()
+ is ViewState.HasData<*> -> onSuccess(it.data)
+ is ViewState.HasError<*> -> onFailure(it.error)
+ }
+ }
+ }
+
+ /**
+ * Called in case of starting operation liveData in viewModel
+ */
+ open fun onStarted() {
+ mActivity?.onStarted()
+ }
+
+ /**
+ * Called in case of success or some data emitted from the liveData in viewModel
+ */
+ open fun onSuccess(data: Any?) {
+ mActivity?.onSuccess(data)
+ }
+
+ /**
+ * Called in case of failure or some error emitted from the liveData in viewModel
+ */
+ open fun onFailure(error: Any?) {
+ mActivity?.onFailure(error)
+ }
+
+ fun setTitle(title: String) {
+ (requireActivity() as BaseActivity).setTitleInActionBar(title)
+ }
+
+ fun popBackStack() = mActivity?.popBackStack()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt
new file mode 100644
index 0000000..fd34388
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt
@@ -0,0 +1,42 @@
+package com.appttude.h_mal.farmr.base
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.appttude.h_mal.farmr.utils.generateView
+
+open class BaseRecyclerAdapter(
+ @LayoutRes private val emptyViewId: Int,
+ @LayoutRes private val currentViewId: Int
+): RecyclerView.Adapter() {
+ var list: List? = null
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return if (list.isNullOrEmpty()) {
+ val emptyViewHolder = parent.generateView(emptyViewId)
+ EmptyViewHolder(emptyViewHolder)
+ } else {
+ val currentViewHolder = parent.generateView(currentViewId)
+ CurrentViewHolder(currentViewHolder)
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return if (list.isNullOrEmpty()) 1 else list!!.size
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ when (holder) {
+ is EmptyViewHolder -> bindEmptyView(holder.itemView)
+ is CurrentViewHolder -> bindCurrentView(holder.itemView, position, list!![position])
+ }
+ }
+
+ open fun bindEmptyView(view: View) {}
+ open fun bindCurrentView(view: View, position: Int, data: T) {}
+
+ class EmptyViewHolder(itemView: View): ViewHolder(itemView)
+ class CurrentViewHolder(itemView: View): ViewHolder(itemView)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt
new file mode 100644
index 0000000..8ff7475
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt
@@ -0,0 +1,25 @@
+package com.appttude.h_mal.farmr.base
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.appttude.h_mal.farmr.model.ViewState
+
+open class BaseViewModel: ViewModel() {
+
+ private val _uiState = MutableLiveData()
+ val uiState: LiveData = _uiState
+
+
+ fun onStart() {
+ _uiState.postValue(ViewState.HasStarted)
+ }
+
+ fun onSuccess(result: T) {
+ _uiState.postValue(ViewState.HasData(result))
+ }
+
+ protected fun onError(error: E) {
+ _uiState.postValue(ViewState.HasError(error))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt
new file mode 100644
index 0000000..db97694
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt
@@ -0,0 +1,24 @@
+package com.appttude.h_mal.farmr.data
+
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.Shift
+import com.appttude.h_mal.farmr.model.Sortable
+
+interface Repository {
+ fun insertShiftIntoDatabase(shift: Shift): Boolean
+ fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean
+ fun readShiftsFromDatabase(): List?
+ fun readSingleShiftFromDatabase(id: Long): ShiftObject?
+ fun deleteSingleShiftFromDatabase(id: Long): Boolean
+ fun deleteAllShiftsFromDatabase(): Boolean
+ fun retrieveSortAndOrderFromPref(): Pair
+ fun setSortAndOrderFromPref(sortable: Sortable, order: Order)
+ fun retrieveFilteringDetailsInPrefs(): Map
+ fun setFilteringDetailsInPrefs(
+ description: String?,
+ timeIn: String?,
+ timeOut: String?,
+ type: String?
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt
new file mode 100644
index 0000000..3af7652
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt
@@ -0,0 +1,71 @@
+package com.appttude.h_mal.farmr.data
+
+import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.Shift
+import com.appttude.h_mal.farmr.model.Sortable
+
+class RepositoryImpl(
+ private val legacyDatabase: LegacyDatabase,
+ private val preferenceProvider: PreferenceProvider
+): Repository {
+ override fun insertShiftIntoDatabase(shift: Shift): Boolean {
+ return legacyDatabase.insertShiftDataIntoDatabase(shift) != null
+ }
+
+ override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean {
+ return legacyDatabase.updateShiftDataIntoDatabase(
+ id = id,
+ typeString = shift.type.type,
+ descriptionString = shift.description,
+ dateString = shift.date,
+ timeInString = shift.timeIn ?: "",
+ timeOutString = shift.timeOut ?: "",
+ duration = shift.duration ?: 0f,
+ breaks = shift.breakMins ?: 0,
+ units = shift.units ?: 0f,
+ payRate = shift.rateOfPay,
+ totalPay = shift.totalPay
+ ) == 1
+ }
+
+ override fun readShiftsFromDatabase(): List? {
+ return legacyDatabase.readShiftsFromDatabase()
+ }
+
+ override fun readSingleShiftFromDatabase(id: Long): ShiftObject? {
+ return legacyDatabase.readSingleShiftWithId(id)
+ }
+
+ override fun deleteSingleShiftFromDatabase(id: Long): Boolean {
+ return legacyDatabase.deleteSingleShift(id) == 1
+ }
+
+ override fun deleteAllShiftsFromDatabase(): Boolean {
+ return legacyDatabase.deleteAllShiftsInDatabase() > 0
+ }
+
+ override fun retrieveSortAndOrderFromPref(): Pair {
+ return preferenceProvider.getSortableAndOrder()
+ }
+
+ override fun setSortAndOrderFromPref(sortable: Sortable, order: Order) {
+ preferenceProvider.saveSortableAndOrder(sortable, order)
+ }
+
+ override fun retrieveFilteringDetailsInPrefs(): Map {
+ return preferenceProvider.getFilteringDetails()
+ }
+
+ override fun setFilteringDetailsInPrefs(
+ description: String?,
+ timeIn: String?,
+ timeOut: String?,
+ type: String?
+ ) {
+ preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt
new file mode 100644
index 0000000..729ea9a
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt
@@ -0,0 +1,166 @@
+package com.appttude.h_mal.farmr.data.legacydb
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.database.Cursor
+import android.net.Uri
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
+import com.appttude.h_mal.farmr.model.Shift
+
+class LegacyDatabase(private val resolver: ContentResolver) {
+
+ private val projection = arrayOf(
+ _ID,
+ COLUMN_SHIFT_DESCRIPTION,
+ COLUMN_SHIFT_DATE,
+ COLUMN_SHIFT_TIME_IN,
+ COLUMN_SHIFT_TIME_OUT,
+ COLUMN_SHIFT_BREAK,
+ COLUMN_SHIFT_DURATION,
+ COLUMN_SHIFT_TYPE,
+ COLUMN_SHIFT_UNIT,
+ COLUMN_SHIFT_PAYRATE,
+ COLUMN_SHIFT_TOTALPAY
+ )
+
+ // Create
+ fun insertShiftDataIntoDatabase(
+ shift: Shift
+ ): Uri? {
+ val values = ContentValues().apply {
+ put(COLUMN_SHIFT_TYPE, shift.type.type)
+ put(COLUMN_SHIFT_DESCRIPTION, shift.description)
+ put(COLUMN_SHIFT_DATE, shift.date)
+ put(COLUMN_SHIFT_TIME_IN, shift.timeIn ?: "00:00")
+ put(COLUMN_SHIFT_TIME_OUT, shift.timeOut ?: "00:00")
+ put(COLUMN_SHIFT_DURATION, shift.duration ?: 0.00f)
+ put(COLUMN_SHIFT_BREAK, shift.breakMins ?: 0)
+ put(COLUMN_SHIFT_UNIT, shift.units ?: 0.00f)
+ put(COLUMN_SHIFT_PAYRATE, shift.rateOfPay)
+ put(COLUMN_SHIFT_TOTALPAY, shift.totalPay)
+ }
+ return resolver.insert(CONTENT_URI, values)
+ }
+
+ // Read
+ fun readShiftsFromDatabase(): List? {
+ val cursor = resolver.query(
+ CONTENT_URI,
+ projection,
+ null, null, null
+ ) ?: return null
+ val shifts = generateSequence { if (cursor.moveToNext()) cursor else null }
+ .map { it.getShift() }
+ .toList()
+ // close cursor after query operations
+ cursor.close()
+
+ return shifts
+ }
+
+ fun readSingleShiftWithId(id: Long): ShiftObject? {
+ val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id)
+
+ val cursor = resolver.query(
+ itemUri,
+ projection,
+ null, null, null
+ ) ?: return null
+ cursor.moveToFirst()
+
+ val shift = cursor.takeIf { it.moveToFirst() }?.run { getShift() } ?: return null
+ cursor.close()
+ return shift
+ }
+
+ // Update
+ fun updateShiftDataIntoDatabase(
+ id: Long,
+ typeString: String,
+ descriptionString: String,
+ dateString: String,
+ timeInString: String,
+ timeOutString: String,
+ duration: Float,
+ breaks: Int,
+ units: Float,
+ payRate: Float,
+ totalPay: Float,
+ ): Int {
+ val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id)
+
+ val values = ContentValues().apply {
+ put(COLUMN_SHIFT_TYPE, typeString)
+ put(COLUMN_SHIFT_DESCRIPTION, descriptionString)
+ put(COLUMN_SHIFT_DATE, dateString)
+ put(COLUMN_SHIFT_TIME_IN, timeInString)
+ put(COLUMN_SHIFT_TIME_OUT, timeOutString)
+ put(COLUMN_SHIFT_DURATION, duration)
+ put(COLUMN_SHIFT_BREAK, breaks)
+ put(COLUMN_SHIFT_UNIT, units)
+ put(COLUMN_SHIFT_PAYRATE, payRate)
+ put(COLUMN_SHIFT_TOTALPAY, totalPay)
+ }
+ return resolver.update(itemUri, values, null, null)
+ }
+
+ // Delete
+ fun deleteAllShiftsInDatabase(): Int {
+ return resolver.delete(CONTENT_URI, null, null)
+ }
+
+ fun deleteSingleShift(id: Long): Int {
+ val args: Array = arrayOf(id.toString())
+ return resolver.delete(CONTENT_URI, "$_ID=?", args)
+ }
+
+ private fun Cursor.getShift(): ShiftObject = run {
+ val id = getLong(getColumnIndexOrThrow(_ID))
+ val descriptionColumnIndex = getString(
+ getColumnIndexOrThrow(
+ COLUMN_SHIFT_DESCRIPTION
+ )
+ )
+ val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE))
+ val timeInColumnIndex =
+ getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN))
+ val timeOutColumnIndex =
+ getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_OUT))
+ val durationColumnIndex =
+ getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_DURATION))
+ val breakOutColumnIndex =
+ getInt(getColumnIndexOrThrow(COLUMN_SHIFT_BREAK))
+ val typeColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TYPE))
+ val unitColumnIndex = getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_UNIT))
+ val payrateColumnIndex =
+ getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_PAYRATE))
+ val totalpayColumnIndex =
+ getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_TOTALPAY))
+
+ ShiftObject(
+ id = id,
+ type = typeColumnIndex,
+ description = descriptionColumnIndex,
+ date = dateColumnIndex,
+ timeIn = timeInColumnIndex,
+ timeOut = timeOutColumnIndex,
+ duration = durationColumnIndex,
+ breakMins = breakOutColumnIndex,
+ units = unitColumnIndex,
+ rateOfPay = payrateColumnIndex,
+ totalPay = totalpayColumnIndex
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt
new file mode 100644
index 0000000..af40553
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt
@@ -0,0 +1,28 @@
+package com.appttude.h_mal.farmr.data.legacydb
+
+import com.appttude.h_mal.farmr.model.Shift
+import com.appttude.h_mal.farmr.model.ShiftType
+import kotlin.math.floor
+
+data class ShiftObject(
+ val id: Long,
+ val type: String,
+ val description: String,
+ val date: String,
+ val timeIn: String,
+ val timeOut: String,
+ val duration: Float,
+ val breakMins: Int,
+ val units: Float,
+ val rateOfPay: Float,
+ val totalPay: Float
+) {
+ fun copyToShift() = Shift(ShiftType.getEnumByType(type), description, date, timeIn, timeOut, duration, breakMins, units, rateOfPay, totalPay)
+
+ fun getHoursMinutesPairFromDuration(): Pair {
+ val hours: Int = floor(duration).toInt()
+ val minutes: Int = ((duration - hours) * 60).toInt()
+ return Pair(hours.toString(), minutes.toString())
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt
similarity index 53%
rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt
rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt
index d6cc534..727fe1f 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt
@@ -1,15 +1,13 @@
-package com.appttude.h_mal.farmr.data
+package com.appttude.h_mal.farmr.data.legacydb
import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
-import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import android.util.Log
-import androidx.annotation.VisibleForTesting
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
/**
* Created by h_mal on 26/12/2017.
@@ -21,22 +19,24 @@ class ShiftProvider : ContentProvider() {
return true
}
- override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?,
- sortOrder: String?): Cursor? {
- var selection = selection
- var selectionArgs = selectionArgs
+ override fun query(
+ uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor {
val database = mDbHelper!!.readableDatabase
- val cursor: Cursor
- val match = sUriMatcher.match(uri)
- when (match) {
- SHIFTS -> cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs,
- null, null, sortOrder)
+ val cursor: Cursor = when (sUriMatcher.match(uri)) {
+ SHIFTS -> database.query(
+ ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs,
+ null, null, sortOrder
+ )
SHIFT_ID -> {
- selection = ShiftsEntry._ID + "=?"
- selectionArgs = arrayOf(ContentUris.parseId(uri).toString())
- cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs,
- null, null, sortOrder)
+ val mSelection = ShiftsEntry._ID + "=?"
+ val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())
+ database.query(
+ ShiftsEntry.TABLE_NAME, projection, mSelection, mSelectionArgs,
+ null, null, sortOrder
+ )
}
else -> throw IllegalArgumentException("Cannot query $uri")
@@ -46,26 +46,25 @@ class ShiftProvider : ContentProvider() {
}
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
- val match = sUriMatcher.match(uri)
- return when (match) {
+ return when (sUriMatcher.match(uri)) {
SHIFTS -> insertShift(uri, contentValues)
else -> throw IllegalArgumentException("Insertion is not supported for $uri")
}
}
private fun insertShift(uri: Uri, values: ContentValues?): Uri? {
- val description = values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
- ?: throw IllegalArgumentException("Description required")
- val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
- ?: throw IllegalArgumentException("Date required")
- val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
- ?: throw IllegalArgumentException("Time In required")
- val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
- ?: throw IllegalArgumentException("Time Out required")
+ values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
+ ?: throw IllegalArgumentException("Description required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
+ ?: throw IllegalArgumentException("Date required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
+ ?: throw IllegalArgumentException("Time In required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
+ ?: throw IllegalArgumentException("Time Out required")
val duration = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_DURATION)
require(duration >= 0) { "Duration cannot be negative" }
- val shiftType = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE)
- ?: throw IllegalArgumentException("Shift type required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE)
+ ?: throw IllegalArgumentException("Shift type required")
val shiftUnits = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_UNIT)
require(shiftUnits >= 0) { "Units cannot be negative" }
val payRate = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_PAYRATE)
@@ -84,43 +83,47 @@ class ShiftProvider : ContentProvider() {
return ContentUris.withAppendedId(uri, id)
}
- override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
- selectionArgs: Array?): Int {
- var selection = selection
- var selectionArgs = selectionArgs
- val match = sUriMatcher.match(uri)
- return when (match) {
+ override fun update(
+ uri: Uri, contentValues: ContentValues?, selection: String?,
+ selectionArgs: Array?
+ ): Int {
+ return when (sUriMatcher.match(uri)) {
SHIFTS -> updateShift(uri, contentValues, selection, selectionArgs)
SHIFT_ID -> {
- selection = ShiftsEntry._ID + "=?"
- selectionArgs = arrayOf(ContentUris.parseId(uri).toString())
- updateShift(uri, contentValues, selection, selectionArgs)
+ val mSelection = ShiftsEntry._ID + "=?"
+ val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())
+ updateShift(uri, contentValues, mSelection, mSelectionArgs)
}
else -> throw IllegalArgumentException("Update is not supported for $uri")
}
}
- private fun updateShift(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
+ private fun updateShift(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?
+ ): Int {
if (values!!.containsKey(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) {
- val description = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
- ?: throw IllegalArgumentException("description required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
+ ?: throw IllegalArgumentException("description required")
}
if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_DATE)) {
- val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
- ?: throw IllegalArgumentException("date required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
+ ?: throw IllegalArgumentException("date required")
}
if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) {
- val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
- ?: throw IllegalArgumentException("time in required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
+ ?: throw IllegalArgumentException("time in required")
}
if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) {
- val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
- ?: throw IllegalArgumentException("time out required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
+ ?: throw IllegalArgumentException("time out required")
}
if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_BREAK)) {
- val breaks = values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK)
- ?: throw IllegalArgumentException("break required")
+ values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK)
+ ?: throw IllegalArgumentException("break required")
}
if (values.size() == 0) {
return 0
@@ -134,17 +137,15 @@ class ShiftProvider : ContentProvider() {
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
- var selection = selection
- var selectionArgs = selectionArgs
val database = mDbHelper!!.writableDatabase
- val rowsDeleted: Int
- val match = sUriMatcher.match(uri)
- when (match) {
- SHIFTS -> rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs)
+ val rowsDeleted: Int = when (sUriMatcher.match(uri)) {
+ SHIFTS -> database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs)
+
SHIFT_ID -> {
- selection = ShiftsEntry._ID + "=?"
- selectionArgs = arrayOf(ContentUris.parseId(uri).toString())
- rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs)
+ val mSelection = ShiftsEntry._ID + "=?"
+ val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString())
+
+ database.delete(ShiftsEntry.TABLE_NAME, mSelection, mSelectionArgs)
}
else -> throw IllegalArgumentException("Deletion is not supported for $uri")
@@ -171,7 +172,11 @@ class ShiftProvider : ContentProvider() {
init {
sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS, SHIFTS)
- sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS + "/#", SHIFT_ID)
+ sUriMatcher.addURI(
+ ShiftsContract.CONTENT_AUTHORITY,
+ ShiftsContract.PATH_SHIFTS + "/#",
+ SHIFT_ID
+ )
}
}
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt
similarity index 96%
rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt
rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt
index ac1a5d0..9fa0a96 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt
@@ -1,4 +1,4 @@
-package com.appttude.h_mal.farmr.data
+package com.appttude.h_mal.farmr.data.legacydb
import android.content.ContentResolver
import android.net.Uri
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt
similarity index 95%
rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt
rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt
index fe83ed6..4918518 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt
@@ -1,9 +1,9 @@
-package com.appttude.h_mal.farmr.data
+package com.appttude.h_mal.farmr.data.legacydb
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
-import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
/**
* Created by h_mal on 26/12/2017.
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt
new file mode 100644
index 0000000..11481c1
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt
@@ -0,0 +1,69 @@
+package com.appttude.h_mal.farmr.data.prefs
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.preference.PreferenceManager
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.Sortable
+
+/**
+ * Shared preferences to save & load last timestamp
+ */
+const val SORT = "SORT"
+const val ORDER = "ORDER"
+
+const val DESCRIPTION = "DESCRIPTION"
+const val DATE_IN = "TIME_IN"
+const val DATE_OUT = "TIME_OUT"
+const val TYPE = "TYPE"
+
+class PreferenceProvider(
+ context: Context
+) {
+
+ private val appContext = context.applicationContext
+
+ private val preference: SharedPreferences
+ get() = PreferenceManager.getDefaultSharedPreferences(appContext)
+
+ fun saveSortableAndOrder(sortable: Sortable, order: Order) {
+ preference.edit()
+ .putString(SORT, sortable.label)
+ .putString(ORDER, order.label)
+ .apply()
+ }
+
+ fun getSortableAndOrder(): Pair {
+ val sort = preference.getString(SORT, null)?.let { Sortable.valueOf(it) }
+ val order = preference.getString(ORDER, null)?.let { Order.valueOf(it) }
+
+ return Pair(sort, order)
+ }
+ fun saveFilteringDetails(
+ description: String?,
+ timeIn: String?,
+ timeOut: String?,
+ type: String?
+ ) {
+ preference.edit()
+ .putString(DESCRIPTION, description)
+ .putString(DATE_IN, timeIn)
+ .putString(DATE_OUT, timeOut)
+ .putString(TYPE, type)
+ .apply()
+ }
+
+ fun getFilteringDetails(): Map {
+ return mapOf(
+ Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)),
+ Pair(DATE_IN, preference.getString(DATE_IN, null)),
+ Pair(DATE_OUT, preference.getString(DATE_OUT, null)),
+ Pair(TYPE, preference.getString(TYPE, null))
+ )
+ }
+
+ fun clearPrefs() {
+ preference.edit().clear().apply()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt
new file mode 100644
index 0000000..cc4b998
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt
@@ -0,0 +1,15 @@
+package com.appttude.h_mal.farmr.di
+
+import com.appttude.h_mal.farmr.base.BaseApplication
+import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
+import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
+
+class ShiftApplication: BaseApplication() {
+
+ override fun createDatabase(): LegacyDatabase {
+ return LegacyDatabase(contentResolver)
+ }
+
+ override fun createPrefs() = PreferenceProvider(this)
+}
+
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt
new file mode 100644
index 0000000..4120cdc
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt
@@ -0,0 +1,11 @@
+package com.appttude.h_mal.farmr.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class EntityItem(
+ @PrimaryKey(autoGenerate = false)
+ val id: String,
+ val shift: Shift
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt
new file mode 100644
index 0000000..45a3e87
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt
@@ -0,0 +1,8 @@
+package com.appttude.h_mal.farmr.model
+
+data class FilterStore(
+ val description: String?,
+ val dateFrom: String?,
+ val dateTo: String?,
+ val type: String?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt
new file mode 100644
index 0000000..b7c971f
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt
@@ -0,0 +1,5 @@
+package com.appttude.h_mal.farmr.model
+
+enum class Order(val label: String) {
+ ASCENDING("Ascending"), DESCENDING("Descending")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt
index 5d76543..4df1de5 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt
@@ -1,14 +1,65 @@
package com.appttude.h_mal.farmr.model
+import com.appttude.h_mal.farmr.utils.calculateDuration
+import com.appttude.h_mal.farmr.utils.formatToTwoDp
+
data class Shift(
- val type: ShiftType,
- val description: String,
- val date: String,
- val timeIn: String?,
- val timeOut: String?,
- val duration: Float?,
- val breakMins: Int?,
- val units: Float?,
- val rateOfPay: Float,
- val totalPay: Float
-)
\ No newline at end of file
+ val type: ShiftType,
+ val description: String,
+ val date: String,
+ val timeIn: String?,
+ val timeOut: String?,
+ val duration: Float?,
+ val breakMins: Int?,
+ val units: Float?,
+ val rateOfPay: Float,
+ val totalPay: Float
+) {
+ companion object {
+ // Invocation for Hourly
+ operator fun invoke(
+ description: String,
+ date: String,
+ timeIn: String,
+ timeOut: String,
+ breakMins: Int? = null,
+ rateOfPay: Float
+ ): Shift {
+ val breakTime = breakMins ?: 0
+ val duration = calculateDuration(timeIn, timeOut, breakTime)
+
+ return Shift(
+ ShiftType.HOURLY,
+ description,
+ date,
+ timeIn,
+ timeOut,
+ duration,
+ breakTime,
+ 0f,
+ rateOfPay,
+ (duration * rateOfPay).formatToTwoDp()
+ )
+ }
+
+ operator fun invoke(
+ description: String,
+ date: String,
+ units: Float,
+ rateOfPay: Float
+ ) = Shift(
+ ShiftType.PIECE,
+ description,
+ date,
+ "",
+ "",
+ 0f,
+ 0,
+ units,
+ rateOfPay,
+ (units * rateOfPay).formatToTwoDp()
+ )
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt
index d4d1118..4087686 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt
@@ -1,6 +1,12 @@
package com.appttude.h_mal.farmr.model
-enum class ShiftType(val type: String) {
+enum class ShiftType(val type: String){
HOURLY("Hourly"),
- PIECE("Piece Rate")
+ PIECE("Piece Rate");
+
+ companion object {
+ fun getEnumByType(type: String): ShiftType {
+ return values().first { it.type == type }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt
new file mode 100644
index 0000000..bb2bbf7
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt
@@ -0,0 +1,17 @@
+package com.appttude.h_mal.farmr.model
+
+enum class Sortable(val label: String) {
+ ID("Default"),
+ TYPE("Shift Type"),
+ DATE("Date"),
+ DESCRIPTION("Description"),
+ DURATION("Added"), UNITS("Duration"),
+ RATEOFPAY("Rate of pay"),
+ TOTALPAY("Total Pay");
+
+ companion object {
+ fun getEnumByType(label: String): Sortable {
+ return Sortable.values().first { it.label == label }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt
new file mode 100644
index 0000000..b7b3d9d
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt
@@ -0,0 +1,5 @@
+package com.appttude.h_mal.farmr.model
+
+data class Success(
+ val successMessage: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt
new file mode 100644
index 0000000..2080f55
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt
@@ -0,0 +1,7 @@
+package com.appttude.h_mal.farmr.model
+
+sealed class ViewState {
+ object HasStarted : ViewState()
+ class HasData(val data: T) : ViewState()
+ class HasError(val error: T) : ViewState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt
new file mode 100644
index 0000000..400c5a4
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt
@@ -0,0 +1,105 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.os.Bundle
+import android.view.View
+import android.view.View.OnClickListener
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Spinner
+import androidx.core.widget.doAfterTextChanged
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BaseFragment
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.model.Success
+import com.appttude.h_mal.farmr.utils.setDatePicker
+import com.appttude.h_mal.farmr.viewmodel.FilterViewModel
+
+class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data),
+ AdapterView.OnItemSelectedListener, OnClickListener {
+ private val spinnerList: Array =
+ arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type)
+
+ private lateinit var LocationET: EditText
+ private lateinit var dateFromET: EditText
+ private lateinit var dateToET: EditText
+ private lateinit var typeSpinner: Spinner
+
+ private var descriptionString: String? = null
+ private var dateFromString: String? = null
+ private var dateToString: String? = null
+ private var typeString: String? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setTitle(getString(R.string.title_activity_filter_data))
+
+ LocationET = view.findViewById(R.id.filterLocationEditText)
+ dateFromET = view.findViewById(R.id.fromdateInEditText)
+ dateToET = view.findViewById(R.id.filterDateOutEditText)
+ typeSpinner = view.findViewById(R.id.TypeFilterEditText)
+ val submit: Button = view.findViewById(R.id.submitFiltered)
+
+ val adapter: ArrayAdapter =
+ ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList)
+ typeSpinner.adapter = adapter
+
+ val filterDetails = viewModel.getFiltrationDetails()
+
+ filterDetails.run {
+ description?.let {
+ LocationET.setText(it)
+ descriptionString = it
+ }
+ dateFrom?.let {
+ dateFromET.setText(it)
+ dateFromString = it
+ }
+ dateTo?.let {
+ dateToET.setText(it)
+ dateToString = it
+ }
+ type?.let {
+ typeString = it
+ val spinnerPosition: Int = adapter.getPosition(it)
+ typeSpinner.setSelection(spinnerPosition)
+ }
+ }
+
+ LocationET.doAfterTextChanged { descriptionString = it.toString() }
+ dateFromET.setDatePicker { dateFromString = it }
+ dateToET.setDatePicker { dateToString = it }
+ typeSpinner.onItemSelectedListener = this
+
+ submit.setOnClickListener(this)
+ }
+
+ override fun onItemSelected(
+ parentView: AdapterView<*>?,
+ selectedItemView: View?,
+ position: Int,
+ id: Long
+ ) {
+ typeString = when (position) {
+ 1 -> ShiftType.HOURLY.type
+ 2 -> ShiftType.PIECE.type
+ else -> return
+ }
+ }
+
+ override fun onNothingSelected(parentView: AdapterView<*>?) {}
+
+ private fun submitFiltrationDetails() {
+ viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString)
+ }
+
+ override fun onClick(p0: View?) {
+ submitFiltrationDetails()
+ }
+
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is Success) popBackStack()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt
new file mode 100644
index 0000000..1b9caf5
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt
@@ -0,0 +1,298 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.RadioButton
+import android.widget.RadioGroup
+import android.widget.ScrollView
+import android.widget.TextView
+import androidx.core.widget.doAfterTextChanged
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BackPressedListener
+import com.appttude.h_mal.farmr.base.BaseFragment
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.model.Success
+import com.appttude.h_mal.farmr.utils.ID
+import com.appttude.h_mal.farmr.utils.createDialog
+import com.appttude.h_mal.farmr.utils.displayToast
+import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
+import com.appttude.h_mal.farmr.utils.formatToTwoDpString
+import com.appttude.h_mal.farmr.utils.hide
+import com.appttude.h_mal.farmr.utils.popBackStack
+import com.appttude.h_mal.farmr.utils.setDatePicker
+import com.appttude.h_mal.farmr.utils.setTimePicker
+import com.appttude.h_mal.farmr.utils.show
+import com.appttude.h_mal.farmr.utils.validateField
+import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
+
+class FragmentAddItem : BaseFragment(R.layout.fragment_add_item),
+ RadioGroup.OnCheckedChangeListener, BackPressedListener {
+
+ private lateinit var mHourlyRadioButton: RadioButton
+ private lateinit var mPieceRadioButton: RadioButton
+ private lateinit var mLocationEditText: EditText
+ private lateinit var mDateEditText: EditText
+ private lateinit var mDurationTextView: TextView
+ private lateinit var mTimeInEditText: EditText
+ private lateinit var mTimeOutEditText: EditText
+ private lateinit var mBreakEditText: EditText
+ private lateinit var mUnitEditText: EditText
+ private lateinit var mPayRateEditText: EditText
+ private lateinit var mTotalPayTextView: TextView
+ private lateinit var hourlyDataView: LinearLayout
+ private lateinit var unitsHolder: LinearLayout
+ private lateinit var durationHolder: LinearLayout
+ private lateinit var wholeView: LinearLayout
+ private lateinit var scrollView: ScrollView
+ private lateinit var submitProduct: Button
+ private lateinit var mRadioGroup: RadioGroup
+
+ private var mDate: String? = null
+ private var mDescription: String? = null
+ private var mTimeIn: String? = null
+ private var mTimeOut: String? = null
+ private var mBreaks: Int? = null
+ private var mUnits: Float? = null
+ private var mPayRate = 0f
+ private var mType: ShiftType? = null
+ private var mDuration: Float? = null
+
+ private var id: Long? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ scrollView = view.findViewById(R.id.total_view)
+ mRadioGroup = view.findViewById(R.id.rg)
+ mHourlyRadioButton = view.findViewById(R.id.hourly)
+ mPieceRadioButton = view.findViewById(R.id.piecerate)
+ mLocationEditText = view.findViewById(R.id.locationEditText)
+ mDateEditText = view.findViewById(R.id.dateEditText)
+ mTimeInEditText = view.findViewById(R.id.timeInEditText)
+ mBreakEditText = view.findViewById(R.id.breakEditText)
+ mTimeOutEditText = view.findViewById(R.id.timeOutEditText)
+ mDurationTextView = view.findViewById(R.id.ShiftDuration)
+ mUnitEditText = view.findViewById(R.id.unitET)
+ mPayRateEditText = view.findViewById(R.id.payrateET)
+ mTotalPayTextView = view.findViewById(R.id.totalpayval)
+ hourlyDataView = view.findViewById(R.id.hourly_data_holder)
+ unitsHolder = view.findViewById(R.id.units_holder)
+ durationHolder = view.findViewById(R.id.duration_holder)
+ wholeView = view.findViewById(R.id.whole_view)
+ submitProduct = view.findViewById(R.id.submit)
+
+ mRadioGroup.setOnCheckedChangeListener(this)
+ mLocationEditText.doAfterTextChanged {
+ mDescription = it.toString()
+ }
+ mDateEditText.setDatePicker { mDate = it }
+ mTimeInEditText.setTimePicker {
+ mTimeIn = it
+ calculateTotalPay()
+ }
+ mTimeOutEditText.setTimePicker {
+ mTimeOut = it
+ calculateTotalPay()
+ }
+ mBreakEditText.doAfterTextChanged {
+ mBreaks = it.toString().toIntOrNull() ?: 0
+ calculateTotalPay()
+ }
+ mUnitEditText.doAfterTextChanged {
+ it.toString().toFloatOrNull()?.let { u -> mUnits = u }
+ calculateTotalPay()
+ }
+ mPayRateEditText.doAfterTextChanged {
+ it.toString().toFloatOrNull()?.let { p ->
+ mPayRate = p
+ calculateTotalPay()
+ }
+ }
+
+ submitProduct.setOnClickListener { submitShift() }
+
+ setupViewAfterViewCreated()
+ }
+
+ private fun setupViewAfterViewCreated() {
+ id = arguments?.getLong(ID)
+ wholeView.hide()
+
+ val title = when (arguments?.containsKey(ID)) {
+ true -> {
+ // Since we are editing a shift lets load the shift data into the views
+ viewModel.getCurrentShift(arguments!!.getLong(ID))?.run {
+ mLocationEditText.setText(description)
+ mDateEditText.setText(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()
+ }
+
+ // Return title
+ getString(R.string.edit_item_title)
+ }
+
+ else -> getString(R.string.add_item_title)
+ }
+ setTitle(title)
+ }
+
+ override fun onCheckedChanged(radioGroup: RadioGroup, id: Int) {
+ when (radioGroup.checkedRadioButtonId) {
+ R.id.hourly -> {
+ mType = ShiftType.HOURLY
+ wholeView.show()
+ unitsHolder.hide()
+ hourlyDataView.show()
+ durationHolder.show()
+ }
+
+ R.id.piecerate -> {
+ mType = ShiftType.PIECE
+ wholeView.show()
+ unitsHolder.show()
+ hourlyDataView.hide()
+ durationHolder.hide()
+ }
+ }
+ }
+
+ private fun submitShift() {
+ mDate.validateField({ !it.isNullOrBlank() }) {
+ onFailure("Date field cannot be empty")
+ return
+ }
+ mDescription.validateField({ !it.isNullOrBlank() }) {
+ onFailure("Description field cannot be empty")
+ return
+ }
+ mPayRate.validateField({ !it.isNaN() }) {
+ onFailure("Rate of pay field cannot be empty")
+ return
+ }
+
+ if (mPieceRadioButton.isChecked) {
+
+ mUnits.validateField({ it != null && it >= 0 }) {
+ onFailure("Units field cannot be empty")
+ return
+ }
+ if (id != null) {
+ // update
+ viewModel.updateShift(
+ id!!,
+ description = mDescription,
+ date = mDate,
+ units = mUnits,
+ rateOfPay = mPayRate
+ )
+ } else {
+ // insert
+ viewModel.insertPieceRateShift(mDescription!!, mDate!!, mUnits!!, mPayRate)
+ }
+ } else if (mHourlyRadioButton.isChecked) {
+ if (id != null) {
+ // update
+ viewModel.updateShift(
+ id!!,
+ description = mDescription,
+ date = mDate,
+ rateOfPay = mPayRate,
+ timeIn = mTimeIn,
+ timeOut = mTimeOut,
+ breakMins = mBreaks
+ )
+ } else {
+ // insert
+ viewModel.insertHourlyShift(
+ mDescription!!,
+ mDate!!,
+ mPayRate,
+ mTimeIn,
+ mTimeOut,
+ mBreaks
+ )
+ }
+
+ }
+ }
+
+ private fun calculateTotalPay() {
+ mType?.let {
+ val total = when (it) {
+ ShiftType.HOURLY -> {
+ // Calculate duration before total pay calculation
+ mDuration = viewModel.retrieveDurationText(mTimeIn, mTimeOut, mBreaks) ?: return
+ mDurationTextView.text =
+ StringBuilder().append(mDuration).append(" hours").toString()
+ mDuration!! * mPayRate
+ }
+ ShiftType.PIECE -> {
+ (mUnits ?: 0f) * mPayRate
+ }
+ }
+ mTotalPayTextView.text = total.formatAsCurrencyString()
+ }
+ }
+
+ override fun onBackPressed(): Boolean {
+ if (mRadioGroup.checkedRadioButtonId == -1) {
+ mActivity?.popBackStack()
+ } else {
+ requireContext().createDialog(
+ title = "Discard Changes?",
+ message = "Are you sure you want to discard changes?",
+ displayCancel = true,
+ okCallback = { _, _ ->
+ mActivity?.popBackStack()
+ }
+ )
+ }
+ return true
+ }
+
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is Success) {
+ displayToast(data.successMessage)
+ popBackStack()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt
new file mode 100644
index 0000000..5e1866b
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt
@@ -0,0 +1,260 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.Manifest
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.widget.Toast
+import androidx.core.app.ActivityCompat
+import androidx.core.content.FileProvider
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BackPressedListener
+import com.appttude.h_mal.farmr.base.BaseFragment
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.Sortable
+import com.appttude.h_mal.farmr.model.Success
+import com.appttude.h_mal.farmr.utils.createDialog
+import com.appttude.h_mal.farmr.utils.displayToast
+import com.appttude.h_mal.farmr.utils.hide
+import com.appttude.h_mal.farmr.utils.navigateToFragment
+import com.appttude.h_mal.farmr.utils.show
+import com.appttude.h_mal.farmr.viewmodel.MainViewModel
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.File
+import kotlin.system.exitProcess
+
+
+class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener {
+ private lateinit var productListView: RecyclerView
+ private lateinit var emptyView: View
+ private lateinit var mAdapter: ShiftListAdapter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setTitle("Shift List")
+ // Inflate the layout for this fragment
+ setHasOptionsMenu(true)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ mAdapter = ShiftListAdapter(this) {
+ viewModel.deleteShift(it)
+ }
+ productListView = view.findViewById(R.id.list_item_view)
+ productListView.adapter = mAdapter
+ emptyView = view.findViewById(R.id.empty_view)
+
+ mAdapter.registerAdapterDataObserver(object : AdapterDataObserver() {
+ override fun onChanged() {
+ super.onChanged()
+ if (mAdapter.itemCount == 0) emptyView.show()
+ else emptyView.hide()
+ }
+ })
+
+ view.findViewById(R.id.fab1).setOnClickListener {
+ navigateToFragment(FragmentAddItem(), name = "additem")
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ viewModel.refreshLiveData()
+ }
+
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is List<*>) {
+ @Suppress("UNCHECKED_CAST")
+ mAdapter.submitList(data as List)
+ }
+ if (data is Success) {
+ displayToast(data.successMessage)
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.delete_all -> {
+ deleteAllProducts()
+ return true
+ }
+
+ R.id.help -> {
+ AlertDialog.Builder(context)
+ .setTitle("Help & Support:")
+ .setView(R.layout.dialog_layout)
+ .setPositiveButton(android.R.string.ok) { arg0, _ -> arg0.dismiss() }
+ .create().show()
+ return true
+ }
+
+ R.id.filter_data -> {
+ navigateToFragment(FilterDataFragment(), name = "filterdata")
+ return true
+ }
+
+ R.id.sort_data -> {
+ sortData()
+ return true
+ }
+
+ R.id.clear_filter -> {
+ viewModel.clearFilters()
+ return true
+ }
+
+ R.id.export_data -> {
+ if (checkStoragePermissions(activity)) {
+ 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()
+ } else {
+ displayToast("Storage permissions required")
+ }
+ return true
+ }
+
+ R.id.action_favorite -> {
+ AlertDialog.Builder(context)
+ .setTitle("Info:")
+ .setMessage(viewModel.getInformation())
+ .setPositiveButton(android.R.string.ok) { arg0, _ ->
+ arg0.dismiss()
+ }.create().show()
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun sortData() {
+ val groupName = Sortable.values().map { it.label }.toTypedArray()
+ var sort = Sortable.ID
+
+ val sortAndOrder = viewModel.getSortAndOrder()
+ val checkedItem = Sortable.values().indexOf(sortAndOrder.first)
+
+ AlertDialog.Builder(context)
+ .setTitle("Sort by:")
+ .setSingleChoiceItems(
+ groupName,
+ checkedItem
+ ) { _, p1 -> sort = Sortable.getEnumByType(groupName[p1]) }
+ .setPositiveButton("Ascending") { dialog, _ ->
+ viewModel.setSortAndOrder(sort)
+ dialog.dismiss()
+ }.setNegativeButton("Descending") { dialog, _ ->
+ viewModel.setSortAndOrder(sort, Order.DESCENDING)
+ dialog.dismiss()
+ }
+ .create().show()
+ }
+
+ private fun deleteAllProducts() {
+ requireContext().createDialog(
+ "Warning",
+ message = "Are you sure you want to delete all date?",
+ displayCancel = true,
+ okCallback = { _, _ ->
+ viewModel.deleteAllShifts()
+ }
+ )
+ }
+
+ private fun exportData() {
+ val permission =
+ ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ if (permission != PackageManager.PERMISSION_GRANTED) {
+ Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show()
+ return
+ }
+
+ val fileName = "shifthistory.xls"
+ val file = File(requireContext().externalCacheDir, fileName)
+
+ viewModel.createExcelSheet(file)?.let {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val excelUri = FileProvider.getUriForFile(
+ requireContext(),
+ requireContext().applicationContext.packageName + ".provider",
+ file
+ )
+ intent.setDataAndType(excelUri, "application/vnd.ms-excel")
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ startActivity(intent)
+ }
+
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ println("request code$requestCode")
+ if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
+ if (grantResults.isNotEmpty()
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ ) {
+ exportDialog()
+ } else {
+ displayToast("Storage Permissions denied")
+ }
+ }
+ }
+
+ private fun exportDialog() {
+ AlertDialog.Builder(context)
+ .setTitle("Export?")
+ .setMessage("Exporting current filtered data. Continue?")
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok) { _, _ -> exportData() }.create().show()
+ }
+
+ private fun checkStoragePermissions(activity: Activity?): Boolean {
+ var status = false
+ val permission = ActivityCompat.checkSelfPermission(
+ activity!!,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+ if (permission == PackageManager.PERMISSION_GRANTED) {
+ status = true
+ }
+ return status
+ }
+
+ companion object {
+ const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
+ }
+
+ override fun onBackPressed(): Boolean {
+ requireContext().createDialog(
+ title = "Leave?",
+ message = "Are you sure you want to exit Farmr?",
+ displayCancel = true,
+ okCallback = { _, _ ->
+ val intent = Intent(Intent.ACTION_MAIN)
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+ intent.addCategory(Intent.CATEGORY_HOME)
+ startActivity(intent)
+ requireActivity().finish()
+ exitProcess(0)
+ }
+ )
+ return true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt
new file mode 100644
index 0000000..be79fa7
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt
@@ -0,0 +1,101 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BaseFragment
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.utils.CURRENCY
+import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
+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.InfoViewModel
+
+class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) {
+ private lateinit var typeTV: TextView
+ private lateinit var descriptionTV: TextView
+ private lateinit var dateTV: TextView
+ private lateinit var times: TextView
+ private lateinit var breakTV: TextView
+ private lateinit var durationTV: TextView
+ private lateinit var unitsTV: TextView
+ private lateinit var payRateTV: TextView
+ private lateinit var totalPayTV: TextView
+ private lateinit var hourlyDetailHolder: LinearLayout
+ private lateinit var unitsHolder: LinearLayout
+ private lateinit var wholeView: LinearLayout
+ private lateinit var progressBarFI: ProgressBar
+ private lateinit var editButton: Button
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setTitle(getString(R.string.further_info_title))
+
+ progressBarFI = view.findViewById(R.id.progressBar_info)
+ wholeView = view.findViewById(R.id.further_info_view)
+ typeTV = view.findViewById(R.id.details_shift)
+ descriptionTV = view.findViewById(R.id.details_desc)
+ dateTV = view.findViewById(R.id.details_date)
+ times = view.findViewById(R.id.details_time)
+ breakTV = view.findViewById(R.id.details_breaks)
+ durationTV = view.findViewById(R.id.details_duration)
+ unitsTV = view.findViewById(R.id.details_units)
+ payRateTV = view.findViewById(R.id.details_pay_rate)
+ totalPayTV = view.findViewById(R.id.details_totalpay)
+ editButton = view.findViewById(R.id.details_edit)
+ hourlyDetailHolder = view.findViewById(R.id.details_hourly_details)
+ unitsHolder = view.findViewById(R.id.details_units_holder)
+
+ editButton.setOnClickListener {
+ navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!)
+ }
+
+ viewModel.retrieveData(arguments)
+ }
+
+ override fun onSuccess(data: Any?) {
+ super.onSuccess(data)
+ if (data is ShiftObject) data.setupView()
+ }
+
+ private fun ShiftObject.setupView() {
+ typeTV.text = type
+ descriptionTV.text = description
+ dateTV.text = date
+ payRateTV.text = rateOfPay.toString()
+ totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString()
+
+ when (ShiftType.getEnumByType(type)) {
+ ShiftType.HOURLY -> {
+ hourlyDetailHolder.show()
+ unitsHolder.hide()
+ times.text = StringBuilder(timeIn).append("-").append(timeOut).toString()
+ breakTV.text = StringBuilder().append(breakMins).append(" mins").toString()
+ durationTV.text = viewModel.buildDurationSummary(this)
+ val paymentSummary =
+ StringBuilder().append(duration).append(" Hours @ ")
+ .append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n")
+ .append("Equals: ").append(totalPay.formatAsCurrencyString())
+ totalPayTV.text = paymentSummary
+ }
+
+ ShiftType.PIECE -> {
+ hourlyDetailHolder.hide()
+ unitsHolder.show()
+ unitsTV.text = units.toString()
+
+ val paymentSummary =
+ StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ")
+ .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n")
+ .append("Equals: ").append(totalPay.formatAsCurrencyString())
+ totalPayTV.text = paymentSummary
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt
new file mode 100644
index 0000000..2bb2763
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt
@@ -0,0 +1,78 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.Manifest
+import android.app.Activity
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.Menu
+import androidx.appcompat.widget.Toolbar
+import androidx.core.app.ActivityCompat
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BackPressedListener
+import com.appttude.h_mal.farmr.base.BaseActivity
+import com.appttude.h_mal.farmr.utils.popBackStack
+
+class MainActivity : BaseActivity() {
+ private lateinit var toolbar: Toolbar
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_view)
+ toolbar = findViewById(R.id.toolbar)
+ setSupportActionBar(toolbar)
+
+ verifyStoragePermissions(this)
+
+ val fragmentTransaction = supportFragmentManager.beginTransaction()
+ fragmentTransaction.replace(R.id.container, FragmentMain()).addToBackStack("main").commit()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ menuInflater.inflate(R.menu.menu_main, menu)
+ return true
+ }
+
+ override fun onBackPressed() {
+ val currentFragment = supportFragmentManager.findFragmentById(R.id.container)
+ if (currentFragment is BackPressedListener) {
+ currentFragment.onBackPressed()
+ } else {
+ if (supportFragmentManager.backStackEntryCount > 1) {
+ popBackStack()
+ } else {
+ super.onBackPressed()
+ }
+ }
+ }
+
+ // Storage Permissions
+ private val REQUEST_EXTERNAL_STORAGE = 1
+ private val PERMISSIONS_STORAGE = arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+
+ /**
+ * Checks if the app has permission to write to device storage
+ *
+ * If the app does not has permission then the user will be prompted to grant permissions
+ *
+ * @param activity
+ */
+ fun verifyStoragePermissions(activity: Activity?) {
+ // Check if we have write permission
+ val permission = ActivityCompat.checkSelfPermission(
+ activity!!,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+ if (permission != PackageManager.PERMISSION_GRANTED) {
+ // We don't have permission so prompt the user
+ ActivityCompat.requestPermissions(
+ activity,
+ PERMISSIONS_STORAGE,
+ REQUEST_EXTERNAL_STORAGE
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt
new file mode 100644
index 0000000..5151661
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt
@@ -0,0 +1,115 @@
+package com.appttude.h_mal.farmr.ui
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.os.Bundle
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import com.appttude.h_mal.farmr.R
+import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.utils.ID
+import com.appttude.h_mal.farmr.utils.generateView
+import com.appttude.h_mal.farmr.utils.navigateToFragment
+
+class ShiftListAdapter(
+ private val fragment: Fragment,
+ private val longPressCallback: (Long) -> Unit
+) : ListAdapter(diffCallBack) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): BaseRecyclerAdapter.CurrentViewHolder {
+ val currentViewHolder = parent.generateView(R.layout.list_item_1)
+ return BaseRecyclerAdapter.CurrentViewHolder(currentViewHolder)
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun onBindViewHolder(holder: BaseRecyclerAdapter.CurrentViewHolder, position: Int) {
+ val view = holder.itemView
+ val data = getItem(position)
+
+ val descriptionTextView: TextView = view.findViewById(R.id.location)
+ val dateTextView: TextView = view.findViewById(R.id.date)
+ val totalPay: TextView = view.findViewById(R.id.total_pay)
+ val hoursView: TextView = view.findViewById(R.id.hours)
+ val h: TextView = view.findViewById(R.id.h)
+ val minutesView: TextView = view.findViewById(R.id.minutes)
+ val m: TextView = view.findViewById(R.id.m)
+ val editView: ImageView = view.findViewById(R.id.imageView)
+ h.text = "h"
+ m.text = "m"
+ val typeText: String = data.type
+ val descriptionText: String = data.description
+ val dateText: String = data.date
+ val totalPayText: String = data.totalPay.toString()
+
+ descriptionTextView.text = descriptionText
+ dateTextView.text = dateText
+ totalPay.text = totalPayText
+
+ when (ShiftType.getEnumByType(typeText)) {
+ ShiftType.HOURLY -> {
+ val time = data.getHoursMinutesPairFromDuration()
+
+ hoursView.text = time.first
+ minutesView.text = time.second
+ }
+
+ ShiftType.PIECE -> {
+ val unitsText: String = data.units.toString()
+
+ hoursView.text = unitsText
+ h.text = ""
+ minutesView.text = ""
+ m.text = "pcs"
+ }
+ }
+
+ val b: Bundle = Bundle()
+ b.putLong(ID, data.id)
+ view.setOnClickListener {
+ // Navigate to further info
+ fragment.navigateToFragment(
+ FurtherInfoFragment(),
+ bundle = b,
+ name = "furtherinfo"
+ )
+ }
+ editView.setOnClickListener {
+ // Navigate to edit
+ fragment.navigateToFragment(
+ FragmentAddItem(),
+ bundle = b,
+ name = "additem"
+ )
+ }
+ view.setOnLongClickListener {
+ AlertDialog.Builder(it.context)
+ .setMessage("Are you sure you want to delete")
+ .setPositiveButton("delete") { _, _ -> longPressCallback.invoke(data.id) }
+ .setNegativeButton("cancel") { dialog, _ ->
+ dialog?.dismiss()
+ }
+ .create().show()
+ true
+ }
+ }
+
+ companion object {
+ val diffCallBack = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
+ return oldItem == newItem
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt
similarity index 50%
rename from app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt
rename to app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt
index 878fb98..5f8cfd8 100644
--- a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt
+++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt
@@ -1,36 +1,32 @@
-package com.appttude.h_mal.farmr
+package com.appttude.h_mal.farmr.ui
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
-import android.view.View
-import android.widget.RelativeLayout
-import androidx.core.app.ActivityOptionsCompat
+import android.os.Looper
+import com.appttude.h_mal.farmr.R
/**
* Created by h_mal on 27/06/2017.
*/
+@SuppressLint("CustomSplashScreen")
class SplashScreen : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
- val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle()
- val relativeLayout = findViewById(R.id.splash_layout) as RelativeLayout
+
val i = Intent(this@SplashScreen, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- Handler().postDelayed({
- // This method will be executed once the timer is over
- // Start your app main activity
-// startActivity(i,bundle);
+ Handler(Looper.getMainLooper()).postDelayed({
startActivity(i)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
- // finish();
- }, SPLASH_TIME_OUT.toLong())
+ }, SPLASH_TIME_OUT)
}
companion object {
// Splash screen timer
- private const val SPLASH_TIME_OUT = 2000
+ const val SPLASH_TIME_OUT: Long = 2000
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt
new file mode 100644
index 0000000..59f48f8
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt
@@ -0,0 +1,7 @@
+package com.appttude.h_mal.farmr.utils
+
+const val LEGACY = "LEGACY_"
+const val DATE_FORMAT = "yyyy-MM-dd"
+const val TIME_FORMAT = "hh:mm"
+const val ID = "ID"
+const val CURRENCY = "£"
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt
new file mode 100644
index 0000000..5b007e1
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt
@@ -0,0 +1,97 @@
+package com.appttude.h_mal.farmr.utils
+
+import java.io.IOException
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Currency
+import java.util.Date
+import java.util.Locale
+
+fun String.formatToTwoDp(): Float {
+ val formattedString = String.format("%.2f", this)
+ return formattedString.toFloat()
+}
+
+fun Float.formatToTwoDp(): Float {
+ val formattedString = String.format("%.2f", this)
+ return formattedString.toFloat()
+}
+
+fun Float.formatAsCurrencyString(): String? {
+ val format: NumberFormat = NumberFormat.getCurrencyInstance()
+ format.maximumFractionDigits = 2
+ format.currency = Currency.getInstance("GBP")
+
+ return format.format(this)
+}
+
+fun Float.formatToTwoDpString(): String {
+ return formatToTwoDp().toString()
+}
+
+fun String.dateStringIsValid(): Boolean {
+ return "([0-9]{4})-([0-9]{2})-([0-9]{2})".toPattern().matcher(this).matches()
+}
+
+fun String.timeStringIsValid(): Boolean {
+ return "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]\$".toPattern().matcher(this).matches()
+}
+
+fun Calendar.getTimeString(): String {
+ val format = SimpleDateFormat(TIME_FORMAT, Locale.getDefault())
+ return format.format(time)
+}
+
+fun String.convertDateString(format: String = DATE_FORMAT): Date? {
+ val formatter = SimpleDateFormat(format, Locale.getDefault())
+ return formatter.parse(this)
+}
+
+/**
+ * turns "HH:mm" into an hour and minutes pair
+ *
+ * eg:
+ * @param 13:45
+ * @return Pair(13, 45)
+ */
+fun convertTimeStringToHourMinutesPair(timeString: String): Pair {
+ val split = timeString.split(":")
+ if (split.size != 2) throw ArrayIndexOutOfBoundsException()
+ return Pair(split.first().toInt(), split[1].toInt())
+}
+
+
+/**
+ * calculate the duration between two 24 hour time strings minus the break in minutes
+ *
+ * can also calculate when time to string in past midnight eg: 23:00, 04:45, 30
+ * @return 5.75
+ */
+fun calculateDuration(timeIn: String, timeOut: String, breaks: Int): Float {
+ val timeFrom = convertTimeStringToHourMinutesPair(timeIn)
+ val timeTo = convertTimeStringToHourMinutesPair(timeOut)
+
+ val hoursIn = timeFrom.first
+ val minutesIn = timeFrom.second
+ val hoursOut = timeTo.first
+ val minutesOut = timeTo.second
+
+ var duration: Float = if (hoursOut > hoursIn) {
+ ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60)))
+ } else {
+ (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24)
+ }
+ if ((breaks.toFloat() / 60) > duration) throw IOException("Breaks duration cannot be larger than shift duration")
+ duration -= (breaks.toFloat() / 60)
+
+ return duration.formatToTwoDp()
+}
+
+fun calculateDuration(timeIn: String?, timeOut: String?, breaks: Int?): Float {
+ val calendar by lazy { Calendar.getInstance() }
+ val insertTimeIn = timeIn ?: calendar.getTimeString()
+ val insertTimeOut = timeOut ?: calendar.getTimeString()
+
+ return calculateDuration(insertTimeIn, insertTimeOut, breaks ?: 0)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt
new file mode 100644
index 0000000..cb0d32c
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt
@@ -0,0 +1,38 @@
+package com.appttude.h_mal.farmr.utils
+
+import com.appttude.h_mal.farmr.model.Order
+import java.lang.reflect.ParameterizedType
+import kotlin.reflect.KClass
+
+@Suppress("UNCHECKED_CAST")
+fun Any.getGenericClassAt(position: Int): KClass =
+ ((javaClass.genericSuperclass as? ParameterizedType)
+ ?.actualTypeArguments?.getOrNull(position) as? Class)
+ ?.kotlin
+ ?: throw IllegalStateException("Can not find class from generic argument")
+
+/**
+ * @param validate when result is false then we trigger
+ * @param onError
+ *
+ *
+ * @sample
+ * var s: String?
+ * i.validate{!i.isNullOrEmpty()} { print("string is empty") }
+ */
+inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Unit) {
+ if (!validate.invoke(this)) {
+ onError.invoke()
+ }
+}
+
+/**
+ * Returns a list of all elements sorted according to the specified comparator. In order of ascending or descending
+ * The sort is stable. It means that equal elements preserve their order relative to each other after sorting.
+ */
+inline fun > Iterable.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List {
+ return when (order) {
+ Order.ASCENDING -> sortedWith(compareBy(selector))
+ Order.DESCENDING -> sortedWith(compareByDescending(selector))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt
new file mode 100644
index 0000000..1e68035
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt
@@ -0,0 +1,177 @@
+package com.appttude.h_mal.farmr.utils
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.app.DatePickerDialog
+import android.app.TimePickerDialog
+import android.content.Context
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.view.inputmethod.InputMethodManager
+import android.widget.EditText
+import android.widget.Toast
+import androidx.annotation.AnimRes
+import androidx.annotation.IdRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import com.appttude.h_mal.farmr.R
+import java.util.Calendar
+
+fun View.show() {
+ this.visibility = View.VISIBLE
+}
+
+fun View.hide() {
+ this.visibility = View.GONE
+}
+
+fun Context.displayToast(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show()
+}
+
+fun Fragment.displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
+}
+
+fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater
+ .from(context)
+ .inflate(layoutId, this, false)
+
+fun Fragment.hideKeyboard() {
+ val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
+ imm?.hideSoftInputFromWindow(view?.windowToken, 0)
+}
+
+fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
+ val animation = AnimationUtils.loadAnimation(context, id)
+ animation.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation)
+ override fun onAnimationStart(a: Animation?) {}
+ override fun onAnimationRepeat(a: Animation?) {}
+ })
+ startAnimation(animation)
+}
+
+fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "") {
+ val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction()
+ fragmentTransaction.replace(container, fragment).addToBackStack(name).commit()
+}
+
+fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "", bundle: Bundle) {
+ val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction()
+ fragmentTransaction.replace(container, fragment.apply { arguments = bundle }).addToBackStack(name).commit()
+}
+
+fun Context.createDialog(
+ title: String?,
+ message: String?,
+ displayCancel: Boolean = false,
+ displayOk: Boolean = true,
+ cancelCallback: DialogInterface.OnClickListener? = null,
+ okCallback: DialogInterface.OnClickListener? = null,
+) {
+ val builder = AlertDialog.Builder(this)
+ title?.let { builder.setTitle(it) }
+ message?.let { builder.setMessage(it) }
+ if (displayCancel) {
+ builder.setNegativeButton(android.R.string.cancel, cancelCallback)
+ }
+ if (displayOk) {
+ builder.setPositiveButton(android.R.string.ok, okCallback)
+ }
+
+ builder.create().show()
+}
+
+fun AppCompatActivity.popBackStack() {
+ supportFragmentManager.popBackStack()
+}
+
+fun EditText.setTimePicker(onSelected: (String) -> Unit) {
+ var mHoursOut: Int
+ var mMinutesOut: Int
+
+ setOnClickListener {
+ val mCurrentTime by lazy { Calendar.getInstance() }
+ if (!text.isNullOrEmpty()) {
+ // EditText contains text - lets try set the parse the text
+ try {
+ val convertedString = convertTimeStringToHourMinutesPair(text.toString())
+ mHoursOut = convertedString.first
+ mMinutesOut = convertedString.second
+ } catch (e: Exception) {
+ mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY]
+ mMinutesOut = mCurrentTime[Calendar.MINUTE]
+ }
+ } else {
+ mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY]
+ mMinutesOut = mCurrentTime[Calendar.MINUTE]
+ }
+ val mTimePicker = TimePickerDialog(this.context,
+ { _, selectedHour, selectedMinute ->
+ val ddTime = String.format("%02d", selectedHour) + ":" + String.format(
+ "%02d",
+ selectedMinute
+ )
+ setText(ddTime)
+ onSelected.invoke(ddTime)
+ }, mHoursOut, mMinutesOut, true
+ ) //Yes 24 hour time
+ mTimePicker.setTitle("Select Time")
+ mTimePicker.show()
+ }
+}
+
+fun EditText.setDatePicker(onSelected: (String) -> Unit) {
+ //To show current date in the datepicker
+ var mYear: Int
+ var mMonth: Int
+ var mDay: Int
+
+ val mCurrentDate by lazy { Calendar.getInstance() }
+
+ if (!text.isNullOrEmpty()) {
+ try {
+ val dateSplit = text.split("-")
+
+ mYear = dateSplit[0].toInt()
+ mMonth = dateSplit[1].toInt()
+ mMonth = if (mMonth == 1) {
+ 0
+ } else {
+ mMonth - 1
+ }
+ mDay = dateSplit[2].toInt()
+ } catch (e: Exception) {
+ mYear = mCurrentDate[Calendar.YEAR]
+ mMonth = mCurrentDate[Calendar.MONTH]
+ mDay = mCurrentDate[Calendar.DAY_OF_MONTH]
+ }
+ } else {
+ mYear = mCurrentDate[Calendar.YEAR]
+ mMonth = mCurrentDate[Calendar.MONTH]
+ mDay = mCurrentDate[Calendar.DAY_OF_MONTH]
+ }
+ val mDatePicker = DatePickerDialog(
+ (this.context),
+ { _, selectedYear, selectedMonth, selectedDay ->
+ var currentMonth = selectedMonth
+ val dateString = StringBuilder().append(selectedYear).append("-")
+ .append(String.format("%02d", (currentMonth + 1.also { currentMonth = it })))
+ .append("-")
+ .append(String.format("%02d", selectedDay))
+ .toString()
+ setText(dateString)
+ onSelected.invoke(dateString)
+ }, mYear, mMonth, mDay
+ )
+ mDatePicker.setTitle("Select date")
+ setOnClickListener {
+ mDatePicker.show()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt
new file mode 100644
index 0000000..73cc067
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt
@@ -0,0 +1,25 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.appttude.h_mal.farmr.data.RepositoryImpl
+
+
+class ApplicationViewModelFactory(
+ private val repository: RepositoryImpl
+) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ with(modelClass) {
+ return when {
+ isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository)
+ isAssignableFrom(SubmissionViewModel::class.java) -> SubmissionViewModel(repository)
+ isAssignableFrom(InfoViewModel::class.java) -> InfoViewModel(repository)
+ isAssignableFrom(FilterViewModel::class.java) -> FilterViewModel(repository)
+ else -> throw IllegalArgumentException("Unknown ViewModel class")
+ } as T
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt
new file mode 100644
index 0000000..87bb2bd
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt
@@ -0,0 +1,21 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import com.appttude.h_mal.farmr.data.Repository
+import com.appttude.h_mal.farmr.model.Success
+
+
+class FilterViewModel(
+ repository: Repository
+) : ShiftViewModel(repository) {
+
+ fun applyFilters(
+ description: String?,
+ dateFrom: String?,
+ dateTo: String?,
+ type: String?
+ ) {
+ super.setFiltrationDetails(description, dateFrom, dateTo, type)
+ onSuccess(Success("Filter(s) have been applied"))
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt
new file mode 100644
index 0000000..025829e
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt
@@ -0,0 +1,39 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import android.os.Bundle
+import com.appttude.h_mal.farmr.data.Repository
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.utils.ID
+
+
+class InfoViewModel(
+ repository: Repository
+) : ShiftViewModel(repository) {
+
+ fun retrieveData(bundle: Bundle?) {
+ val id = bundle?.getLong(ID)
+ if (id == null) {
+ onError("Failed to retrieve shift")
+ return
+ }
+
+ val shift = getCurrentShift(id)
+ if (shift == null) {
+ onError("Failed to retrieve shift")
+ return
+ }
+
+ onSuccess(shift)
+ }
+
+ fun buildDurationSummary(shiftObject: ShiftObject): String {
+ val time = shiftObject.getHoursMinutesPairFromDuration()
+
+ val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second)
+ .append(" Minutes ")
+ if (shiftObject.breakMins > 0) {
+ stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)")
+ }
+ return stringBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt
new file mode 100644
index 0000000..7929615
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt
@@ -0,0 +1,289 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import androidx.annotation.RequiresPermission
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import com.appttude.h_mal.farmr.data.Repository
+import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
+import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
+import com.appttude.h_mal.farmr.model.Order
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.model.Sortable
+import com.appttude.h_mal.farmr.model.Success
+import com.appttude.h_mal.farmr.utils.convertDateString
+import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
+import com.appttude.h_mal.farmr.utils.sortedByOrder
+import jxl.Workbook
+import jxl.WorkbookSettings
+import jxl.write.Label
+import jxl.write.WritableWorkbook
+import jxl.write.WriteException
+import java.io.File
+import java.io.IOException
+import java.util.Locale
+
+
+class MainViewModel(
+ private val repository: Repository
+) : ShiftViewModel(repository) {
+
+ private val _shiftLiveData = MutableLiveData>()
+ private val shiftLiveData: LiveData> = _shiftLiveData
+
+ private var mSort: Sortable = Sortable.ID
+ private var mOrder: Order = Order.ASCENDING
+
+ private val observer = Observer> {
+ it?.let {
+ val result = it.applyFilters().sortList(mSort, mOrder)
+ onSuccess(result)
+ }
+ }
+
+ init {
+ // Load shifts into live data when view model has been instantiated
+ refreshLiveData()
+ shiftLiveData.observeForever(observer)
+ }
+
+ private fun List.applyFilters(): List {
+ val filter = getFiltrationDetails()
+
+ return filter { s ->
+ comparedStrings(filter.type, s.type) &&
+ comparedStringsContains(filter.description, s.description) &&
+ (isBetween(filter.dateFrom, filter.dateTo, s.date) ?: true)
+ }
+ }
+
+ private fun comparedStrings(first: String?, second: String?): Boolean {
+ return when (compareValues(first, second)) {
+ -1, 0, 1 -> true
+ else -> {
+ false
+ }
+ }
+ }
+
+ private fun comparedStringsContains(first: String?, second: String?): Boolean {
+ first?.let {
+ (second?.contains(it))?.let { c -> return c }
+ }
+
+ return comparedStrings(first, second)
+ }
+
+ private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? {
+ val first = fromDate?.convertDateString()
+ val second = toDate?.convertDateString()
+
+ if (first == null && second == null) return null
+ val compareDate = compareWith.convertDateString() ?: return null
+
+ if (second == null) return compareDate.after(first)
+ if (first == null) return compareDate.before(second)
+
+ return compareDate.after(first) && compareDate.before(second)
+ }
+
+
+ override fun onCleared() {
+ shiftLiveData.removeObserver(observer)
+ super.onCleared()
+ }
+
+ private fun List.sortList(sort: Sortable, order: Order): List {
+ return when (sort) {
+ Sortable.ID -> sortedByOrder(order) { it.id }
+ Sortable.TYPE -> sortedByOrder(order) { it.type }
+ Sortable.DATE -> sortedByOrder(order) { it.date }
+ Sortable.DESCRIPTION -> sortedByOrder(order) { it.description }
+ Sortable.DURATION -> sortedByOrder(order) { it.duration }
+ Sortable.UNITS -> sortedByOrder(order) { it.units }
+ Sortable.RATEOFPAY -> sortedByOrder(order) { it.rateOfPay }
+ Sortable.TOTALPAY -> sortedByOrder(order) { it.totalPay }
+ }
+ }
+
+ fun getSortAndOrder(): Pair {
+ return Pair(mSort, mOrder)
+ }
+
+ fun setSortAndOrder(sort: Sortable, order: Order = Order.ASCENDING) {
+ mSort = sort
+ mOrder = order
+ refreshLiveData()
+ }
+
+ fun getInformation(): String {
+ var totalDuration = 0.0f
+ var countOfTypeH = 0
+ var countOfTypeP = 0
+ var totalUnits = 0f
+ var totalPay = 0f
+ var lines = 0
+ _shiftLiveData.value?.applyFilters()?.forEach {
+ lines += 1
+ totalDuration += it.duration
+ when (ShiftType.getEnumByType(it.type)) {
+ ShiftType.HOURLY -> countOfTypeH += 1
+ ShiftType.PIECE -> countOfTypeP += 1
+ }
+ totalUnits += it.units
+ totalPay += it.totalPay
+ }
+
+ return buildInfoString(
+ totalDuration,
+ countOfTypeH,
+ countOfTypeP,
+ totalUnits,
+ totalPay,
+ lines
+ )
+ }
+
+ fun deleteShift(id: Long) {
+ if (!repository.deleteSingleShiftFromDatabase(id)) {
+ onError("Failed to delete shift")
+ } else {
+ refreshLiveData()
+ }
+ }
+
+ fun deleteAllShifts() {
+ if (!repository.deleteAllShiftsFromDatabase()) {
+ onError("Failed to delete all shifts from database")
+ } else {
+ refreshLiveData()
+ }
+ }
+
+ private fun buildInfoString(
+ totalDuration: Float,
+ countOfHourly: Int,
+ countOfPiece: Int,
+ totalUnits: Float,
+ totalPay: Float,
+ lines: Int
+ ): String {
+ val stringBuilder = StringBuilder("$lines Shifts").append("\n")
+
+ if (countOfHourly != 0 && countOfPiece != 0) {
+ stringBuilder.append(" ($countOfHourly Hourly/$countOfPiece Piece Rate)").append("\n")
+ }
+ if (countOfHourly != 0) {
+ stringBuilder.append("Total Hours: ").append(totalDuration).append("\n")
+ }
+ if (countOfPiece != 0) {
+ stringBuilder.append("Total Units: ").append(totalUnits).append("\n")
+ }
+ if (totalPay != 0f) {
+ stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString())
+ }
+ return stringBuilder.toString()
+ }
+
+ fun refreshLiveData() {
+ repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) }
+ }
+
+ fun clearFilters() {
+ super.setFiltrationDetails(null, null, null, null)
+ onSuccess(Success("Filters have been cleared"))
+ refreshLiveData()
+ }
+
+ @RequiresPermission(WRITE_EXTERNAL_STORAGE)
+ fun createExcelSheet(file: File): File? {
+ val wbSettings = WorkbookSettings().apply {
+ locale = Locale("en", "EN")
+ }
+
+ try {
+ val workbook: WritableWorkbook = Workbook.createWorkbook(file, wbSettings)
+ val sheet = workbook.createSheet("Shifts", 0)
+ // Write column headers
+ val headers = listOf(
+ Label(0, 0, _ID),
+ Label(1, 0, COLUMN_SHIFT_TYPE),
+ Label(2, 0, COLUMN_SHIFT_DESCRIPTION),
+ Label(3, 0, COLUMN_SHIFT_DATE),
+ Label(4, 0, COLUMN_SHIFT_TIME_IN),
+ Label(5, 0, COLUMN_SHIFT_TIME_OUT),
+ Label(6, 0, "$COLUMN_SHIFT_BREAK (in mins)"),
+ Label(7, 0, COLUMN_SHIFT_DURATION),
+ Label(8, 0, COLUMN_SHIFT_UNIT),
+ Label(9, 0, COLUMN_SHIFT_PAYRATE),
+ Label(10, 0, COLUMN_SHIFT_TOTALPAY)
+ )
+ // table content
+ if (shiftLiveData.value.isNullOrEmpty()) {
+ onError("No data to parse into excel file")
+ return null
+ }
+ val sortAndOrder = getSortAndOrder()
+ val data = shiftLiveData.value!!.applyFilters()
+ .sortList(sortAndOrder.first, sortAndOrder.second)
+ var currentRow = 0
+ val cells = data.map { shift ->
+ currentRow += 1
+ listOf(
+ Label(0, currentRow, shift.id.toString()),
+ Label(1, currentRow, shift.type),
+ Label(2, currentRow, shift.description),
+ Label(3, currentRow, shift.date),
+ Label(4, currentRow, shift.timeIn),
+ Label(5, currentRow, shift.timeOut),
+ Label(6, currentRow, shift.breakMins.toString()),
+ Label(7, currentRow, shift.duration.toString()),
+ Label(8, currentRow, shift.units.toString()),
+ Label(9, currentRow, shift.rateOfPay.toString()),
+ Label(10, currentRow, shift.totalPay.toString())
+ )
+ }.flatten()
+
+ currentRow += 1
+ val footer = listOf(
+ Label(0, currentRow, "Total:"),
+ Label(7, currentRow, data.sumOf { it.duration.toDouble() }.toString()),
+ Label(8, currentRow, data.sumOf { it.units.toDouble() }.toString()),
+ Label(10, currentRow, data.sumOf { it.totalPay.toDouble() }.toString())
+ )
+ val content = listOf(headers, cells, footer).flatten()
+
+ // Write content to sheet
+ try {
+ content.forEach { c -> sheet.addCell(c) }
+ } catch (e: WriteException) {
+ onError("Failed to write excel sheet")
+ return null
+ } catch (e: WriteException) {
+ onError("Failed to write excel sheet")
+ return null
+ }
+
+ workbook.write()
+ workbook.close()
+
+ return file
+ } catch (e: IOException) {
+ e.printStackTrace()
+ onError("Failed to generate excel sheet of shifts")
+ }
+ return null
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt
new file mode 100644
index 0000000..abcc710
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt
@@ -0,0 +1,52 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import com.appttude.h_mal.farmr.base.BaseViewModel
+import com.appttude.h_mal.farmr.data.Repository
+import com.appttude.h_mal.farmr.data.prefs.DATE_IN
+import com.appttude.h_mal.farmr.data.prefs.DATE_OUT
+import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
+import com.appttude.h_mal.farmr.data.prefs.TYPE
+import com.appttude.h_mal.farmr.model.FilterStore
+
+
+open class ShiftViewModel(
+ private val repository: Repository
+) : BaseViewModel() {
+
+ /*
+ * Add Item & Further info
+ */
+ fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id)
+
+ /**
+ * Lambda function that will invoke onError(...) on failure
+ * but update live data when successful
+ */
+ private inline fun doTry(operation: () -> Unit) {
+ try {
+ operation.invoke()
+ } catch (e: Exception) {
+ onError(e)
+ }
+ }
+
+ open fun setFiltrationDetails(
+ description: String?,
+ dateFrom: String?,
+ dateTo: String?,
+ type: String?
+ ) {
+ repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type)
+ }
+
+ open fun getFiltrationDetails(): FilterStore {
+ val prefs = repository.retrieveFilteringDetailsInPrefs()
+ return FilterStore(
+ prefs[DESCRIPTION],
+ prefs[DATE_IN],
+ prefs[DATE_OUT],
+ prefs[TYPE]
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt
new file mode 100644
index 0000000..322ca40
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt
@@ -0,0 +1,308 @@
+package com.appttude.h_mal.farmr.viewmodel
+
+import com.appttude.h_mal.farmr.data.Repository
+import com.appttude.h_mal.farmr.model.Shift
+import com.appttude.h_mal.farmr.model.ShiftType
+import com.appttude.h_mal.farmr.model.Success
+import com.appttude.h_mal.farmr.utils.calculateDuration
+import com.appttude.h_mal.farmr.utils.dateStringIsValid
+import com.appttude.h_mal.farmr.utils.formatToTwoDp
+import com.appttude.h_mal.farmr.utils.getTimeString
+import com.appttude.h_mal.farmr.utils.timeStringIsValid
+import java.io.IOException
+import java.util.Calendar
+
+
+class SubmissionViewModel(
+ private val repository: Repository
+) : ShiftViewModel(repository) {
+
+ fun insertHourlyShift(
+ description: String,
+ date: String,
+ rateOfPay: Float,
+ timeIn: String?,
+ timeOut: String?,
+ breakMins: Int?,
+ ) {
+ // Validate inputs from the edit texts
+ (description.length > 3).validateField {
+ onError("Description length should be longer")
+ return
+ }
+ date.dateStringIsValid().validateField {
+ onError("Date format is invalid")
+ return
+ }
+ (rateOfPay >= 0.00).validateField {
+ onError("Rate of pay is invalid")
+ return
+ }
+ timeIn?.timeStringIsValid()?.validateField {
+ onError("Time in format is in correct")
+ return
+ }
+ timeOut?.timeStringIsValid()?.validateField {
+ onError("Time out format is in correct")
+ return
+ }
+ breakMins?.let { it >= 0 }?.validateField {
+ onError("Break in minutes is invalid")
+ return
+ }
+
+ val result = insertShiftIntoDatabase(
+ ShiftType.HOURLY,
+ description,
+ date,
+ rateOfPay.formatToTwoDp(),
+ timeIn,
+ timeOut,
+ breakMins,
+ null
+ )
+
+ if (result) onSuccess(Success("New shift successfully added"))
+ else onError("Cannot insert shift")
+ }
+
+ fun insertPieceRateShift(
+ description: String,
+ date: String,
+ units: Float,
+ rateOfPay: Float
+ ) {
+ // Validate inputs from the edit texts
+ (description.length > 3).validateField {
+ onError("Description length should be longer")
+ return
+ }
+ date.dateStringIsValid().validateField {
+ onError("Date format is invalid")
+ return
+ }
+ (rateOfPay >= 0.00).validateField {
+ onError("Rate of pay is invalid")
+ return
+ }
+ (units.toInt() >= 0).validateField {
+ onError("Units cannot be below zero")
+ return
+ }
+
+ val result = insertShiftIntoDatabase(
+ type = ShiftType.PIECE,
+ description = description,
+ date = date,
+ rateOfPay = rateOfPay.formatToTwoDp(),
+ null,
+ null,
+ null,
+ units = units
+ )
+ if (result) onSuccess(Success("New shift successfully added"))
+ else onError("Cannot insert shift")
+ }
+
+ fun updateShift(
+ id: Long,
+ type: String? = null,
+ description: String? = null,
+ date: String? = null,
+ rateOfPay: Float? = null,
+ timeIn: String? = null,
+ timeOut: String? = null,
+ breakMins: Int? = null,
+ units: Float? = null,
+ ) {
+ description?.let {
+ (it.length > 3).validateField {
+ onError("Description length should be longer")
+ return
+ }
+ }
+ date?.dateStringIsValid()?.validateField {
+ onError("Date format is invalid")
+ return
+ }
+ rateOfPay?.let {
+ (it >= 0.00).validateField {
+ onError("Rate of pay is invalid")
+ return
+ }
+ }
+ units?.let {
+ (it.toInt() >= 0).validateField {
+ onError("Units cannot be below zero")
+ return
+ }
+ }
+ timeIn?.timeStringIsValid()?.validateField {
+ onError("Time in format is in correct")
+ return
+ }
+ timeOut?.timeStringIsValid()?.validateField {
+ onError("Time out format is in correct")
+ return
+ }
+ breakMins?.let { it >= 0 }?.validateField {
+ onError("Break in minutes is invalid")
+ return
+ }
+
+ val result = updateShiftInDatabase(
+ id,
+ type = type?.let { ShiftType.getEnumByType(it) },
+ description = description,
+ date = date,
+ rateOfPay = rateOfPay,
+ timeIn = timeIn,
+ timeOut = timeOut,
+ breakMins = breakMins,
+ units = units
+ )
+
+ if (result) onSuccess(Success("Shift successfully updated"))
+ else onError("Cannot update shift")
+ }
+
+ private fun updateShiftInDatabase(
+ id: Long,
+ type: ShiftType? = null,
+ description: String? = null,
+ date: String? = null,
+ rateOfPay: Float? = null,
+ timeIn: String? = null,
+ timeOut: String? = null,
+ breakMins: Int? = null,
+ units: Float? = null,
+ ): Boolean {
+ val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift()
+ ?: throw IOException("Cannot update shift as it does not exist")
+
+ val shift = when (type) {
+ ShiftType.HOURLY -> {
+ // Shift type has changed so mandatory fields for hourly shift are now required as well
+ val insertTimeIn =
+ (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted")
+ val insertTimeOut =
+ (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted")
+ Shift(
+ description = description ?: currentShift.description,
+ date = date ?: currentShift.date,
+ timeIn = insertTimeIn,
+ timeOut = insertTimeOut,
+ breakMins = breakMins ?: currentShift.breakMins,
+ rateOfPay = rateOfPay ?: currentShift.rateOfPay
+ )
+ }
+
+ ShiftType.PIECE -> {
+ // Shift type has changed so mandatory fields for piece rate shift are now required as well
+ val insertUnits = (units ?: currentShift.units)
+ ?: throw IOException("Units must be inserted for piece rate shifts")
+ Shift(
+ description = description ?: currentShift.description,
+ date = date ?: currentShift.date,
+ units = insertUnits,
+ rateOfPay = rateOfPay ?: currentShift.rateOfPay
+ )
+ }
+
+ else -> {
+ if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) {
+ // Updates to description or date field
+ currentShift.copy(
+ description = description ?: currentShift.description,
+ date = date ?: currentShift.date,
+ )
+ } else {
+ // Updating shifts where shift type has remained the same
+ when (currentShift.type) {
+ ShiftType.HOURLY -> {
+ val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException(
+ "No time in inserted"
+ )
+ val insertTimeOut = (timeOut ?: currentShift.timeOut)
+ ?: throw IOException("No time out inserted")
+ Shift(
+ description = description ?: currentShift.description,
+ date = date ?: currentShift.date,
+ timeIn = insertTimeIn,
+ timeOut = insertTimeOut,
+ breakMins = breakMins ?: currentShift.breakMins,
+ rateOfPay = rateOfPay ?: currentShift.rateOfPay
+ )
+ }
+
+ ShiftType.PIECE -> {
+ val insertUnits = (units ?: currentShift.units)
+ ?: throw IOException("Units must be inserted for piece rate shifts")
+ Shift(
+ description = description ?: currentShift.description,
+ date = date ?: currentShift.date,
+ units = insertUnits,
+ rateOfPay = rateOfPay ?: currentShift.rateOfPay
+ )
+ }
+ }
+ }
+ }
+ }
+
+ return repository.updateShiftIntoDatabase(id, shift)
+ }
+
+ private fun insertShiftIntoDatabase(
+ type: ShiftType,
+ description: String,
+ date: String,
+ rateOfPay: Float,
+ timeIn: String?,
+ timeOut: String?,
+ breakMins: Int?,
+ units: Float?,
+ ): Boolean {
+ val shift = when (type) {
+ ShiftType.HOURLY -> {
+ if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null")
+ val calendar by lazy { Calendar.getInstance() }
+ val insertTimeIn = timeIn ?: calendar.getTimeString()
+ val insertTimeOut = timeOut ?: calendar.getTimeString()
+ Shift(
+ description = description,
+ date = date,
+ timeIn = insertTimeIn,
+ timeOut = insertTimeOut,
+ breakMins = breakMins,
+ rateOfPay = rateOfPay
+ )
+ }
+
+ ShiftType.PIECE -> {
+ Shift(
+ description = description,
+ date = date,
+ units = units!!,
+ rateOfPay = rateOfPay,
+ )
+ }
+ }
+
+ return repository.insertShiftIntoDatabase(shift)
+ }
+
+ private inline fun Boolean.validateField(failureCallback: () -> Unit) {
+ if (!this) failureCallback.invoke()
+ }
+
+ fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? {
+ try {
+ return calculateDuration(mTimeIn, mTimeOut, mBreaks)
+ } catch (e: IOException) {
+ onError(e)
+ }
+ return null
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/farm.jpg b/app/src/main/res/drawable/farm.jpg
deleted file mode 100644
index 290da71..0000000
Binary files a/app/src/main/res/drawable/farm.jpg and /dev/null differ
diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml
deleted file mode 100644
index 34b8202..0000000
--- a/app/src/main/res/drawable/ic_info_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index ca3826a..0000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_release_background.xml b/app/src/main/res/drawable/ic_launcher_release_background.xml
deleted file mode 100644
index ca3826a..0000000
--- a/app/src/main/res/drawable/ic_launcher_release_background.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml
deleted file mode 100644
index e3400cf..0000000
--- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml
deleted file mode 100644
index 2aef437..0000000
--- a/app/src/main/res/drawable/ic_sync_black_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/img_i_full.png b/app/src/main/res/drawable/img_i_full.png
deleted file mode 100644
index 97c195c..0000000
Binary files a/app/src/main/res/drawable/img_i_full.png and /dev/null differ
diff --git a/app/src/main/res/layout/empty_list_view.xml b/app/src/main/res/layout/empty_list_view.xml
new file mode 100644
index 0000000..8112b34
--- /dev/null
+++ b/app/src/main/res/layout/empty_list_view.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_add_item.xml b/app/src/main/res/layout/fragment_add_item.xml
index 8f6152b..fd3bf6f 100644
--- a/app/src/main/res/layout/fragment_add_item.xml
+++ b/app/src/main/res/layout/fragment_add_item.xml
@@ -8,16 +8,9 @@
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.appttude.h_mal.farmr.FragmentAddItem"
+ tools:context="com.appttude.h_mal.farmr.ui.FragmentAddItem"
android:orientation="vertical">
-
-
diff --git a/app/src/main/res/layout/fragment_filter_data.xml b/app/src/main/res/layout/fragment_filter_data.xml
index 3b67fa9..3ebe08f 100644
--- a/app/src/main/res/layout/fragment_filter_data.xml
+++ b/app/src/main/res/layout/fragment_filter_data.xml
@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.appttude.h_mal.farmr.FilterDataFragment">
+ tools:context="com.appttude.h_mal.farmr.ui.FilterDataFragment">
+ tools:context="com.appttude.h_mal.farmr.ui.FurtherInfoFragment">
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml
index 3d50c40..e9fd9c1 100644
--- a/app/src/main/res/layout/fragment_main.xml
+++ b/app/src/main/res/layout/fragment_main.xml
@@ -2,44 +2,16 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- xmlns:ads="http://schemas.android.com/apk/res-auto"
- tools:context="com.appttude.h_mal.farmr.FragmentMain">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:context="com.appttude.h_mal.farmr.ui.FragmentMain">
-
-
-
-
-
-
-
-
-
+
+ app:backgroundTint="@color/colorPrimary" />
+
+
diff --git a/app/src/main/res/layout/list_item_1.xml b/app/src/main/res/layout/list_item_1.xml
index 3ff54df..bdec9ea 100644
--- a/app/src/main/res/layout/list_item_1.xml
+++ b/app/src/main/res/layout/list_item_1.xml
@@ -126,7 +126,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
-
android:layout_alignParentTop="true"
app:srcCompat="@android:drawable/ic_menu_edit" />
diff --git a/app/src/main/res/layout/main_view.xml b/app/src/main/res/layout/main_view.xml
index 790957a..5dcff32 100644
--- a/app/src/main/res/layout/main_view.xml
+++ b/app/src/main/res/layout/main_view.xml
@@ -2,7 +2,7 @@
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 0135299..e4980fb 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -1,7 +1,7 @@