mirror of
https://github.com/hmalik144/Farmr.git
synced 2026-01-31 02:41:49 +00:00
Merge pull request #19 from hmalik144/modern_architecture
Modern architecture MVVM clean architecture test suite expansion CI/CD fastlane implementation
This commit is contained in:
@@ -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
|
||||
102
.gitignore
vendored
102
.gitignore
vendored
@@ -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
|
||||
|
||||
22
.idea/androidTestResultsUserPreferences.xml
generated
22
.idea/androidTestResultsUserPreferences.xml
generated
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidTestResultsUserPreferences">
|
||||
<option name="androidTestResultsTableState">
|
||||
<map>
|
||||
<entry key="1283002349">
|
||||
<value>
|
||||
<AndroidTestResultsTableState>
|
||||
<option name="preferredColumnWidths">
|
||||
<map>
|
||||
<entry key="Duration" value="90" />
|
||||
<entry key="Pixel_6_Pro_API_31" value="120" />
|
||||
<entry key="Tests" value="360" />
|
||||
</map>
|
||||
</option>
|
||||
</AndroidTestResultsTableState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
127
.idea/assetWizardSettings.xml
generated
127
.idea/assetWizardSettings.xml
generated
@@ -1,127 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WizardSettings">
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="imageWizard">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="imageAssetPanel">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="actionbar">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="opacityPercent" value="80" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="theme" value="HOLO_DARK" />
|
||||
<entry key="themeColor" value="ffffff" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="launcher">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="foregroundImage">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="scalingPercent" value="69" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="foregroundImage" value="C:\Users\h_mal\Desktop\Farmr\farmicon.png" />
|
||||
<entry key="outputName" value="ic_launcher_release" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="launcherLegacy">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="notification">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
BIN
.idea/caches/build_file_checksums.ser
generated
BIN
.idea/caches/build_file_checksums.ser
generated
Binary file not shown.
BIN
.idea/caches/gradle_models.ser
generated
BIN
.idea/caches/gradle_models.ser
generated
Binary file not shown.
113
.idea/codeStyles/Project.xml
generated
113
.idea/codeStyles/Project.xml
generated
@@ -1,113 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
21
.idea/gradle.xml
generated
21
.idea/gradle.xml
generated
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="false" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
30
.idea/jarRepositories.xml
generated
30
.idea/jarRepositories.xml
generated
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven" />
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://jitpack.io" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
<option name="version" value="1.7.10" />
|
||||
</component>
|
||||
</project>
|
||||
47
.idea/misc.xml
generated
47
.idea/misc.xml
generated
@@ -1,47 +0,0 @@
|
||||
<project version="4">
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="12">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="android.annotation.Nullable" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="11">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="android.annotation.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/modules.xml
generated
12
.idea/modules.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Farmr.iml" filepath="$PROJECT_DIR$/.idea/modules/Farmr.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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<Shift>) = shifts.forEach { addToDatabase(it) }
|
||||
fun clearDatabase() = database.deleteAllShiftsInDatabase()
|
||||
fun cleanPrefs() = preferenceProvider.clearPrefs()
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
107
app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
Normal file
107
app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
Normal file
@@ -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<A : Activity>(
|
||||
private val activity: Class<A>,
|
||||
private val intentBundle: Bundle? = null,
|
||||
) {
|
||||
|
||||
lateinit var scenario: ActivityScenario<A>
|
||||
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<View> = 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()
|
||||
}
|
||||
@@ -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 <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollTo<VH>(
|
||||
hasDescendant(withText(text))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> scrollToRecyclerItem(
|
||||
recyclerId: Int,
|
||||
resIdForString: Int
|
||||
): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollTo<VH>(
|
||||
hasDescendant(withText(resIdForString))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(
|
||||
recyclerId: Int,
|
||||
position: Int
|
||||
): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollToPosition<VH>(position)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.actionOnItem<VH>(
|
||||
hasDescendant(withText(resIdForString)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickRecyclerAtPosition(recyclerId: Int, position: Int) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollToPosition<VH>(position),
|
||||
RecyclerViewActions.actionOnItemAtPosition<VH>(position, click()),
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollToPosition<VH>(position),
|
||||
RecyclerViewActions.actionOnItemAtPosition<VH>(position, object : ViewAction {
|
||||
override fun getDescription(): String {
|
||||
return "click on subview in RecyclerView at position: $position"
|
||||
}
|
||||
|
||||
override fun getConstraints(): Matcher<View> {
|
||||
return Matchers.allOf(
|
||||
isAssignableFrom(
|
||||
RecyclerView::class.java
|
||||
), isDisplayed()
|
||||
)
|
||||
}
|
||||
|
||||
override fun perform(uiController: UiController?, view: View?) {
|
||||
view?.findViewById<View>(subViewId)?.performClick()
|
||||
}
|
||||
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickOnRecyclerItemWithText(recyclerId: Int, text: String) {
|
||||
matchView(recyclerId).perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollTo<VH>(
|
||||
hasDescendant(withText(text))
|
||||
),
|
||||
RecyclerViewActions.actionOnItem<VH>(
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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<CurrentViewHolder>(R.id.list_item_view, text)
|
||||
fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
|
||||
fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position, R.id.imageView)
|
||||
fun clickFab() = clickButton(R.id.fab1)
|
||||
fun 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>(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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.appttude.h_mal.farmr.ui.utils
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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<View>): ViewAction {
|
||||
|
||||
return object : ViewAction {
|
||||
|
||||
override fun getConstraints(): Matcher<View> = 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<View> = 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<View> {
|
||||
return object : BaseMatcher<View>() {
|
||||
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<View>,
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 <T> LiveData<T>.getOrAwaitValue(
|
||||
time: Long = 2,
|
||||
timeUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): T {
|
||||
var data: T? = null
|
||||
val latch = CountDownLatch(1)
|
||||
val observer = object : Observer<T> {
|
||||
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
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".di.ShiftApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@@ -16,7 +17,7 @@
|
||||
|
||||
<!-- Splash screen -->
|
||||
<activity
|
||||
android:name="com.appttude.h_mal.farmr.SplashScreen"
|
||||
android:name="com.appttude.h_mal.farmr.ui.SplashScreen"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar"
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
@@ -27,15 +28,25 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.appttude.h_mal.farmr.MainActivity"
|
||||
android:parentActivityName="com.appttude.h_mal.farmr.SplashScreen"
|
||||
android:name="com.appttude.h_mal.farmr.ui.MainActivity"
|
||||
android:parentActivityName="com.appttude.h_mal.farmr.ui.SplashScreen"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:exported="true"/>
|
||||
|
||||
<provider
|
||||
android:name="com.appttude.h_mal.farmr.data.ShiftProvider"
|
||||
android:name="com.appttude.h_mal.farmr.data.legacydb.ShiftProvider"
|
||||
android:authorities="com.appttude.h_mal.farmr"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_path" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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<String> = arrayOf("", "Hourly", "Piece Rate")
|
||||
private val listArgs: MutableList<String> = 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<View>(R.id.filterLocationEditText) as EditText?
|
||||
dateFromET = rootView.findViewById<View>(R.id.fromdateInEditText) as EditText?
|
||||
dateToET = rootView.findViewById<View>(R.id.filterDateOutEditText) as EditText?
|
||||
typeSpinner = rootView.findViewById<View>(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<String> = 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<View>(R.id.submitFiltered) as Button
|
||||
submit.setOnClickListener {
|
||||
BuildQuery()
|
||||
activity.args = listArgs.toTypedArray<String>()
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Cursor?> {
|
||||
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<View>(R.id.pd_ai) as ProgressBar
|
||||
scrollView = rootView.findViewById<View>(R.id.total_view) as ScrollView
|
||||
mRadioGroup = rootView.findViewById<View>(R.id.rg) as RadioGroup
|
||||
mRadioButtonOne = rootView.findViewById<View>(R.id.hourly) as RadioButton
|
||||
mRadioButtonTwo = rootView.findViewById<View>(R.id.piecerate) as RadioButton
|
||||
mLocationEditText = rootView.findViewById<View>(R.id.locationEditText) as EditText
|
||||
mDateEditText = rootView.findViewById<View>(R.id.dateEditText) as EditText
|
||||
mTimeInEditText = rootView.findViewById<View>(R.id.timeInEditText) as EditText
|
||||
mBreakEditText = rootView.findViewById<View>(R.id.breakEditText) as EditText
|
||||
mTimeOutEditText = rootView.findViewById<View>(R.id.timeOutEditText) as EditText
|
||||
mDurationTextView = rootView.findViewById<View>(R.id.ShiftDuration) as TextView
|
||||
mUnitEditText = rootView.findViewById<View>(R.id.unitET) as EditText
|
||||
mPayRateEditText = rootView.findViewById<View>(R.id.payrateET) as EditText
|
||||
mTotalPayTextView = rootView.findViewById<View>(R.id.totalpayval) as TextView
|
||||
hourlyDataView = rootView.findViewById<View>(R.id.hourly_data_holder) as LinearLayout
|
||||
unitsHolder = rootView.findViewById<View>(R.id.units_holder) as LinearLayout
|
||||
durationHolder = rootView.findViewById<View>(R.id.duration_holder) as LinearLayout
|
||||
wholeView = rootView.findViewById<View>(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<View>(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<Cursor?> {
|
||||
progressBarAI!!.visibility = View.VISIBLE
|
||||
scrollView!!.visibility = View.GONE
|
||||
val projection = arrayOf<String?>(
|
||||
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<Cursor?>) {}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Cursor?>, 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
|
||||
}
|
||||
}
|
||||
@@ -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<Cursor> {
|
||||
var mCursorAdapter: ShiftsCursorAdapter? = null
|
||||
var shiftsDbhelper: ShiftsDbHelper? = null
|
||||
lateinit var defaultLoaderCallback: LoaderManager.LoaderCallbacks<Cursor>
|
||||
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<View>(R.id.list_item_view) as ListView
|
||||
val emptyView = rootView.findViewById<View>(R.id.empty_view)
|
||||
productListView.emptyView = emptyView
|
||||
mCursorAdapter = ShiftsCursorAdapter(activity, null)
|
||||
productListView.adapter = mCursorAdapter
|
||||
loaderManager.initLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
|
||||
val fab = rootView.findViewById<FloatingActionButton>(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<String?>("")
|
||||
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<String?>(
|
||||
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<String?>(
|
||||
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<Cursor> {
|
||||
val projection = arrayOf<String?>(
|
||||
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: Cursor) {
|
||||
mCursorAdapter!!.swapCursor(cursor)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<Cursor>) {
|
||||
mCursorAdapter!!.swapCursor(null)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Cursor> {
|
||||
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<View>(R.id.progressBar_info) as ProgressBar?
|
||||
wholeView = rootView.findViewById<View>(R.id.further_info_view) as LinearLayout?
|
||||
typeTV = rootView.findViewById<View>(R.id.details_shift) as TextView?
|
||||
descriptionTV = rootView.findViewById<View>(R.id.details_desc) as TextView?
|
||||
dateTV = rootView.findViewById<View>(R.id.details_date) as TextView?
|
||||
times = rootView.findViewById<View>(R.id.details_time) as TextView?
|
||||
breakTV = rootView.findViewById<View>(R.id.details_breaks) as TextView?
|
||||
durationTV = rootView.findViewById<View>(R.id.details_duration) as TextView?
|
||||
unitsTV = rootView.findViewById<View>(R.id.details_units) as TextView?
|
||||
payRateTV = rootView.findViewById<View>(R.id.details_pay_rate) as TextView?
|
||||
totalPayTV = rootView.findViewById<View>(R.id.details_totalpay) as TextView?
|
||||
editButton = rootView.findViewById<View>(R.id.details_edit) as Button?
|
||||
hourlyDetailHolder = rootView.findViewById<View>(R.id.details_hourly_details) as LinearLayout?
|
||||
unitsHolder = rootView.findViewById<View>(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<Cursor?> {
|
||||
progressBarFI!!.visibility = View.VISIBLE
|
||||
wholeView!!.visibility = View.GONE
|
||||
val projection: Array<String?> = 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: 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<Cursor>) {}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_LOADER: Int = 0
|
||||
}
|
||||
}
|
||||
@@ -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<String>? = 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<View>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<View>(R.id.location) as TextView
|
||||
val dateTextView: TextView = view.findViewById<View>(R.id.date) as TextView
|
||||
val totalPay: TextView = view.findViewById<View>(R.id.total_pay) as TextView
|
||||
val hoursView: TextView = view.findViewById<View>(R.id.hours) as TextView
|
||||
val h: TextView = view.findViewById<View>(R.id.h) as TextView
|
||||
val minutesView: TextView = view.findViewById<View>(R.id.minutes) as TextView
|
||||
val m: TextView = view.findViewById<View>(R.id.m) as TextView
|
||||
val editView: ImageView = view.findViewById<View>(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<String> = 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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.appttude.h_mal.farmr.base
|
||||
|
||||
interface BackPressedListener {
|
||||
fun onBackPressed(): Boolean
|
||||
}
|
||||
@@ -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 <A : AppCompatActivity> startActivity(activity: Class<A>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int) :
|
||||
Fragment(contentLayoutId), KodeinAware {
|
||||
|
||||
override val kodein by kodein()
|
||||
private val factory by instance<ApplicationViewModelFactory>()
|
||||
|
||||
val viewModel: V by getViewModel()
|
||||
|
||||
private fun getViewModel(): Lazy<V> =
|
||||
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
|
||||
factoryProducer = { factory } )
|
||||
|
||||
var mActivity: BaseActivity? = null
|
||||
|
||||
private var shortAnimationDuration by Delegates.notNull<Int>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
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()
|
||||
}
|
||||
@@ -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<T: Any>(
|
||||
@LayoutRes private val emptyViewId: Int,
|
||||
@LayoutRes private val currentViewId: Int
|
||||
): RecyclerView.Adapter<ViewHolder>() {
|
||||
var list: List<T>? = 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)
|
||||
}
|
||||
@@ -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<ViewState>()
|
||||
val uiState: LiveData<ViewState> = _uiState
|
||||
|
||||
|
||||
fun onStart() {
|
||||
_uiState.postValue(ViewState.HasStarted)
|
||||
}
|
||||
|
||||
fun <T : Any> onSuccess(result: T) {
|
||||
_uiState.postValue(ViewState.HasData(result))
|
||||
}
|
||||
|
||||
protected fun <E : Any> onError(error: E) {
|
||||
_uiState.postValue(ViewState.HasError(error))
|
||||
}
|
||||
}
|
||||
@@ -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<ShiftObject>?
|
||||
fun readSingleShiftFromDatabase(id: Long): ShiftObject?
|
||||
fun deleteSingleShiftFromDatabase(id: Long): Boolean
|
||||
fun deleteAllShiftsFromDatabase(): Boolean
|
||||
fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?>
|
||||
fun setSortAndOrderFromPref(sortable: Sortable, order: Order)
|
||||
fun retrieveFilteringDetailsInPrefs(): Map<String, String?>
|
||||
fun setFilteringDetailsInPrefs(
|
||||
description: String?,
|
||||
timeIn: String?,
|
||||
timeOut: String?,
|
||||
type: String?
|
||||
)
|
||||
}
|
||||
@@ -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<ShiftObject>? {
|
||||
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<Sortable?, Order?> {
|
||||
return preferenceProvider.getSortableAndOrder()
|
||||
}
|
||||
|
||||
override fun setSortAndOrderFromPref(sortable: Sortable, order: Order) {
|
||||
preferenceProvider.saveSortableAndOrder(sortable, order)
|
||||
}
|
||||
|
||||
override fun retrieveFilteringDetailsInPrefs(): Map<String, String?> {
|
||||
return preferenceProvider.getFilteringDetails()
|
||||
}
|
||||
|
||||
override fun setFilteringDetailsInPrefs(
|
||||
description: String?,
|
||||
timeIn: String?,
|
||||
timeOut: String?,
|
||||
type: String?
|
||||
) {
|
||||
preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String?>(
|
||||
_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<ShiftObject>? {
|
||||
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<String> = 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> {
|
||||
val hours: Int = floor(duration).toInt()
|
||||
val minutes: Int = ((duration - hours) * 60).toInt()
|
||||
return Pair(hours.toString(), minutes.toString())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String>?, selection: String?, selectionArgs: Array<String>?,
|
||||
sortOrder: String?): Cursor? {
|
||||
var selection = selection
|
||||
var selectionArgs = selectionArgs
|
||||
override fun query(
|
||||
uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?,
|
||||
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<String>?): 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<String>?
|
||||
): 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<String>?): Int {
|
||||
private fun updateShift(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
): 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<String>?): 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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<Sortable?, Order?> {
|
||||
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<String, String?> {
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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?
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.appttude.h_mal.farmr.model
|
||||
|
||||
enum class Order(val label: String) {
|
||||
ASCENDING("Ascending"), DESCENDING("Descending")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
17
app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt
Normal file
17
app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.appttude.h_mal.farmr.model
|
||||
|
||||
data class Success(
|
||||
val successMessage: String
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.appttude.h_mal.farmr.model
|
||||
|
||||
sealed class ViewState {
|
||||
object HasStarted : ViewState()
|
||||
class HasData<T : Any>(val data: T) : ViewState()
|
||||
class HasError<T : Any>(val error: T) : ViewState()
|
||||
}
|
||||
@@ -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<FilterViewModel>(R.layout.fragment_filter_data),
|
||||
AdapterView.OnItemSelectedListener, OnClickListener {
|
||||
private val spinnerList: Array<String> =
|
||||
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<String> =
|
||||
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()
|
||||
}
|
||||
}
|
||||
298
app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt
Normal file
298
app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt
Normal file
@@ -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<SubmissionViewModel>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
260
app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt
Normal file
260
app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt
Normal file
@@ -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<MainViewModel>(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<FloatingActionButton>(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<ShiftObject>)
|
||||
}
|
||||
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<String>,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<InfoViewModel>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ShiftObject, BaseRecyclerAdapter.CurrentViewHolder>(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<ShiftObject>() {
|
||||
override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<View>(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
|
||||
}
|
||||
}
|
||||
@@ -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 = "£"
|
||||
@@ -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<Int, Int> {
|
||||
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)
|
||||
}
|
||||
@@ -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 <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
|
||||
((javaClass.genericSuperclass as? ParameterizedType)
|
||||
?.actualTypeArguments?.getOrNull(position) as? Class<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: Any?> 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 <T, R : Comparable<R>> Iterable<T>.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List<T> {
|
||||
return when (order) {
|
||||
Order.ASCENDING -> sortedWith(compareBy(selector))
|
||||
Order.DESCENDING -> sortedWith(compareByDescending(selector))
|
||||
}
|
||||
}
|
||||
177
app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt
Normal file
177
app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<List<ShiftObject>>()
|
||||
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
|
||||
|
||||
private var mSort: Sortable = Sortable.ID
|
||||
private var mOrder: Order = Order.ASCENDING
|
||||
|
||||
private val observer = Observer<List<ShiftObject>> {
|
||||
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<ShiftObject>.applyFilters(): List<ShiftObject> {
|
||||
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<ShiftObject>.sortList(sort: Sortable, order: Order): List<ShiftObject> {
|
||||
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<Sortable, Order> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB |
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
|
||||
</vector>
|
||||
@@ -1,74 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
@@ -1,74 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
android:height="108dp"
|
||||
android:width="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-0.25 1.97,-0.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z" />
|
||||
</vector>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
38
app/src/main/res/layout/empty_list_view.xml
Normal file
38
app/src/main/res/layout/empty_list_view.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<RelativeLayout
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_title_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingTop="16dp"
|
||||
android:text="Shift list empty"
|
||||
android:textAppearance="?android:textAppearanceMedium"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_subtitle_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/empty_title_text"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:fontFamily="sans-serif"
|
||||
android:paddingTop="8dp"
|
||||
android:text="add shift to begin"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="#A2AAB0"/>
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -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">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pd_ai"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -189,7 +182,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Break"
|
||||
android:text="@string/break_res"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat" />
|
||||
|
||||
<EditText
|
||||
@@ -198,7 +191,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:ems="10"
|
||||
android:hint="Break in minutes"
|
||||
android:hint="@string/insert_break_in_minutes"
|
||||
android:inputType="number"
|
||||
android:selectAllOnFocus="true" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -7,7 +7,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.FurtherInfoFragment">
|
||||
tools:context="com.appttude.h_mal.farmr.ui.FurtherInfoFragment">
|
||||
|
||||
<ProgressBar
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/recycler">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</FrameLayout>
|
||||
@@ -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">
|
||||
|
||||
<ListView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_item_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/list_item_1">
|
||||
</ListView>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_title_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:paddingTop="16dp"
|
||||
android:text="Shift list empty"
|
||||
android:textAppearance="?android:textAppearanceMedium"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_subtitle_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/empty_title_text"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:fontFamily="sans-serif"
|
||||
android:paddingTop="8dp"
|
||||
android:text="add shift to begin"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="#A2AAB0"/>
|
||||
</RelativeLayout>
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab1"
|
||||
@@ -49,6 +21,11 @@
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:src="@drawable/add"
|
||||
ads:backgroundTint="@color/colorPrimary" />
|
||||
app:backgroundTint="@color/colorPrimary" />
|
||||
|
||||
<include
|
||||
android:visibility="gone"
|
||||
layout="@layout/empty_list_view"
|
||||
android:id="@+id/empty_view"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context=".MainActivity"
|
||||
tools:context=".ui.MainActivity"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="com.appttude.h_mal.farmr.MainActivity">
|
||||
tools:context="com.appttude.h_mal.farmr.ui.MainActivity">
|
||||
<item
|
||||
android:id="@+id/action_favorite"
|
||||
android:icon="@drawable/image_i_64"
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<resources>
|
||||
<string name="app_name">Farmr</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="category_ach">Shifts</string>
|
||||
|
||||
<string name="add_item_title">Add Shift</string>
|
||||
<string name="edit_item_title">Edit Shift</string>
|
||||
<string name="delete_item">Delete Shift</string>
|
||||
<string name="insert_item_failed">failed to insert Shift</string>
|
||||
<string name="insert_item_successful">Shift successfully added</string>
|
||||
<string name="update_item_failed">Update Failed</string>
|
||||
@@ -25,11 +23,6 @@
|
||||
<!-- Example General settings -->
|
||||
<string name="pref_header_general">General</string>
|
||||
|
||||
<string name="pref_title_social_recommendations">Enable social recommendations</string>
|
||||
<string name="pref_description_social_recommendations">Recommendations for people to contact
|
||||
based on your message history
|
||||
</string>
|
||||
|
||||
<string name="pref_title_display_name">Display name</string>
|
||||
<string name="pref_default_display_name">John Smith</string>
|
||||
|
||||
@@ -102,4 +95,6 @@
|
||||
<!-- TODO: Remove or change this placeholder text -->
|
||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||
<string name="further_info_title">Shift Details</string>
|
||||
<string name="insert_break_in_minutes">insert break in minutes</string>
|
||||
<string name="break_res">Break</string>
|
||||
</resources>
|
||||
|
||||
4
app/src/main/res/xml/provider_path.xml
Normal file
4
app/src/main/res/xml/provider_path.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path name="external_files" path="."/>
|
||||
</paths>
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.appttude.h_mal.farmr;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() throws Exception {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.appttude.h_mal.farmr.data
|
||||
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class RepositoryImplTest {
|
||||
|
||||
private lateinit var repository: RepositoryImpl
|
||||
|
||||
@MockK
|
||||
lateinit var db: LegacyDatabase
|
||||
|
||||
@MockK
|
||||
lateinit var prefs: PreferenceProvider
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
repository = RepositoryImpl(db, prefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readDatabase_validResponse() {
|
||||
// Arrange
|
||||
val elements = listOf<ShiftObject>(
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() }
|
||||
)
|
||||
|
||||
//Act
|
||||
every { db.readShiftsFromDatabase() } returns elements
|
||||
|
||||
// Assert
|
||||
val result = repository.readShiftsFromDatabase()
|
||||
assertIs<List<ShiftObject>>(result)
|
||||
assertEquals(result.first().id, anyLong())
|
||||
}
|
||||
|
||||
}
|
||||
149
app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt
Normal file
149
app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.appttude.h_mal.farmr.utils
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mockito.ArgumentMatchers
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
fun <T> LiveData<T>.getOrAwaitValue(
|
||||
time: Long = 2,
|
||||
timeUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): T {
|
||||
var data: T? = null
|
||||
val latch = CountDownLatch(1)
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(o: T?) {
|
||||
data = o
|
||||
latch.countDown()
|
||||
this@getOrAwaitValue.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
this.observeForever(observer)
|
||||
|
||||
// Don't wait indefinitely if the LiveData is not set.
|
||||
if (!latch.await(time, timeUnit)) {
|
||||
throw TimeoutException("LiveData value was never set.")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return data as T
|
||||
}
|
||||
|
||||
fun sleep(millis: Long = 1000) {
|
||||
runBlocking(Dispatchers.Default) { delay(millis) }
|
||||
}
|
||||
|
||||
fun getShifts() = listOf(
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
"Day one",
|
||||
"2023-08-01",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
"Day two",
|
||||
"2023-08-02",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
"Day three",
|
||||
"2023-08-03",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
30,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
5f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
"Day four",
|
||||
"2023-08-04",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
30,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
5f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
"Day five",
|
||||
"2023-08-05",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
"Day six",
|
||||
"2023-08-06",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
"Day seven",
|
||||
"2023-08-07",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
"Day eight",
|
||||
"2023-08-08",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.appttude.h_mal.farmr.viewmodel
|
||||
|
||||
import android.os.Bundle
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.utils.ID
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
|
||||
|
||||
@Test
|
||||
fun retrieveData_validBundleAndId_successfulRetrieval() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<ShiftObject>()
|
||||
val bundle = mockk<Bundle>()
|
||||
|
||||
// Act
|
||||
every { repository.readSingleShiftFromDatabase(id) }.returns(shift)
|
||||
every { bundle.getLong(ID) }.returns(id)
|
||||
viewModel.retrieveData(bundle)
|
||||
|
||||
// Assert
|
||||
assertIs<ShiftObject>(retrieveCurrentData())
|
||||
assertEquals(
|
||||
retrieveCurrentData(),
|
||||
shift
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<ShiftObject>()
|
||||
val bundle = mockk<Bundle>()
|
||||
|
||||
// Act
|
||||
every { repository.readSingleShiftFromDatabase(id) }.returns(shift)
|
||||
every { bundle.getLong(ID) }.returns(id)
|
||||
viewModel.retrieveData(null)
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
retrieveCurrentError(),
|
||||
"Failed to retrieve shift"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveData_validBundleNoShift_successfulRetrieval() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val bundle = mockk<Bundle>()
|
||||
|
||||
// Act
|
||||
every { repository.readSingleShiftFromDatabase(id) }.returns(null)
|
||||
every { bundle.getLong(ID) }.returns(id)
|
||||
viewModel.retrieveData(bundle)
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
retrieveCurrentError(),
|
||||
"Failed to retrieve shift"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildDurationSummary_validHourlyShift_successfulRetrieval() {
|
||||
// Arrange
|
||||
val shift = getShifts()[0]
|
||||
val shiftWithBreak = getShifts()[3]
|
||||
|
||||
// Act
|
||||
val summary = viewModel.buildDurationSummary(shift)
|
||||
val summaryWithBreak = viewModel.buildDurationSummary(shiftWithBreak)
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
"1 Hours 0 Minutes ",
|
||||
summary
|
||||
)
|
||||
assertEquals(
|
||||
"1 Hours 0 Minutes (+ 30 minutes break)",
|
||||
summaryWithBreak
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user