- mid commit

This commit is contained in:
2023-08-28 21:13:43 +01:00
parent a614dfe543
commit 07d7e6cbe7
29 changed files with 1646 additions and 457 deletions

View File

@@ -1,26 +1,10 @@
package com.appttude.h_mal.farmr.base
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelLazy
import com.appttude.h_mal.farmr.utils.displayToast
import com.appttude.h_mal.farmr.utils.getGenericClassAt
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
abstract class BaseActivity<V : BaseViewModel> : AppCompatActivity(), 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 } )
abstract class BaseActivity : AppCompatActivity() {
/**

View File

@@ -5,11 +5,13 @@ import android.view.View
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.createViewModelLazy
import androidx.lifecycle.ViewModelLazy
import com.appttude.h_mal.farmr.model.ViewState
import com.appttude.h_mal.farmr.utils.getGenericClassAt
import com.appttude.h_mal.farmr.utils.popBackStack
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance
import kotlin.properties.Delegates
@@ -21,14 +23,13 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
override val kodein by kodein()
private val factory by instance<ApplicationViewModelFactory>()
val viewModel: V by getActivityViewModel()
val viewModel: V by getViewModel()
private fun getActivityViewModel() = createViewModelLazy<V>(
getGenericClassAt(0),
{ requireActivity().viewModelStore },
{ factory })
private fun getViewModel(): Lazy<V> =
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
factoryProducer = { factory } )
var mActivity: BaseActivity<*>? = null
var mActivity: BaseActivity? = null
private var shortAnimationDuration by Delegates.notNull<Int>()
@@ -39,7 +40,7 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mActivity = requireActivity() as BaseActivity<*>
mActivity = requireActivity() as BaseActivity
configureObserver()
}
@@ -75,7 +76,7 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
}
fun setTitle(title: String) {
(requireActivity() as BaseActivity<*>).setTitleInActionBar(title)
(requireActivity() as BaseActivity).setTitleInActionBar(title)
}
fun popBackStack() = mActivity?.popBackStack()

View File

@@ -13,8 +13,8 @@ const val SORT = "SORT"
const val ORDER = "ORDER"
const val DESCRIPTION = "DESCRIPTION"
const val TIME_IN = "TIME_IN"
const val TIME_OUT = "TIME_OUT"
const val DATE_IN = "TIME_IN"
const val DATE_OUT = "TIME_OUT"
const val TYPE = "TYPE"
class PreferenceProvider(
@@ -47,8 +47,8 @@ class PreferenceProvider(
) {
preference.edit()
.putString(DESCRIPTION, description)
.putString(TIME_IN, timeIn)
.putString(TIME_OUT, timeOut)
.putString(DATE_IN, timeIn)
.putString(DATE_OUT, timeOut)
.putString(TYPE, type)
.apply()
}
@@ -56,8 +56,8 @@ class PreferenceProvider(
fun getFilteringDetails(): Map<String, String?> {
return mapOf(
Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)),
Pair(TIME_IN, preference.getString(TIME_IN, null)),
Pair(TIME_OUT, preference.getString(TIME_OUT, null)),
Pair(DATE_IN, preference.getString(DATE_IN, null)),
Pair(DATE_OUT, preference.getString(DATE_OUT, null)),
Pair(TYPE, preference.getString(TYPE, null))
)
}

View File

@@ -14,9 +14,9 @@ import com.appttude.h_mal.farmr.base.BaseFragment
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.setDatePicker
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
import com.appttude.h_mal.farmr.viewmodel.FilterViewModel
class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_data),
class FilterDataFragment : BaseFragment<FilterViewModel>(R.layout.fragment_filter_data),
AdapterView.OnItemSelectedListener, OnClickListener {
private val spinnerList: Array<String> =
arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type)
@@ -26,10 +26,10 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
private lateinit var dateToET: EditText
private lateinit var typeSpinner: Spinner
private var description: String? = null
private var dateFrom: String? = null
private var dateTo: String? = null
private var type: String? = null
private var descriptionString: String? = null
private var dateFromString: String? = null
private var dateToString: String? = null
private var typeString: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -47,21 +47,29 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
val filterDetails = viewModel.getFiltrationDetails()
filterDetails.let {
LocationET.setText(it.description)
dateFromET.setText(it.dateFrom)
dateToET.setText(it.dateTo)
it.type?.let { t ->
val spinnerPosition: Int = adapter.getPosition(t)
filterDetails.run {
description?.let {
LocationET.setText(it)
descriptionString = it
}
dateFrom?.let {
dateFromET.setText(it)
dateFromString = it
}
dateTo?.let {
dateToET.setText(it)
dateToString = it
}
type?.let {
typeString = it
val spinnerPosition: Int = adapter.getPosition(it)
typeSpinner.setSelection(spinnerPosition)
}
}
LocationET.doAfterTextChanged { description = it.toString() }
dateFromET.setDatePicker { dateFrom = it }
dateToET.setDatePicker { dateTo = it }
LocationET.doAfterTextChanged { descriptionString = it.toString() }
dateFromET.setDatePicker { dateFromString = it }
dateToET.setDatePicker { dateToString = it }
typeSpinner.onItemSelectedListener = this
submit.setOnClickListener(this)
@@ -73,7 +81,7 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
position: Int,
id: Long
) {
type = when (position) {
typeString = when (position) {
1 -> ShiftType.HOURLY.type
2 -> ShiftType.PIECE.type
else -> return
@@ -83,7 +91,7 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
override fun onNothingSelected(parentView: AdapterView<*>?) {}
private fun submitFiltrationDetails() {
viewModel.setFiltrationDetails(description, dateFrom, dateTo, type)
viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString)
}
override fun onClick(p0: View?) {

View File

@@ -26,8 +26,9 @@ import com.appttude.h_mal.farmr.utils.setTimePicker
import com.appttude.h_mal.farmr.utils.show
import com.appttude.h_mal.farmr.utils.validateField
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
class FragmentAddItem : BaseFragment<MainViewModel>(R.layout.fragment_add_item),
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item),
RadioGroup.OnCheckedChangeListener, BackPressedListener {
private lateinit var mHourlyRadioButton: RadioButton
@@ -262,7 +263,6 @@ class FragmentAddItem : BaseFragment<MainViewModel>(R.layout.fragment_add_item),
StringBuilder().append(mDuration).append(" hours").toString()
mDuration!! * mPayRate
}
ShiftType.PIECE -> {
(mUnits ?: 0f) * mPayRate
}

View File

@@ -70,7 +70,6 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
override fun onStart() {
super.onStart()
viewModel.refreshLiveData()
}
@@ -112,7 +111,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
}
R.id.clear_filter -> {
viewModel.setFiltrationDetails(null, null, null, null)
viewModel.clearFilters()
return true
}

View File

@@ -11,13 +11,14 @@ import com.appttude.h_mal.farmr.base.BaseFragment
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.utils.CURRENCY
import com.appttude.h_mal.farmr.utils.ID
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
import com.appttude.h_mal.farmr.utils.formatToTwoDpString
import com.appttude.h_mal.farmr.utils.hide
import com.appttude.h_mal.farmr.utils.navigateToFragment
import com.appttude.h_mal.farmr.utils.show
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
import com.appttude.h_mal.farmr.viewmodel.InfoViewModel
class FurtherInfoFragment : BaseFragment<MainViewModel>(R.layout.fragment_futher_info) {
class FurtherInfoFragment : BaseFragment<InfoViewModel>(R.layout.fragment_futher_info) {
private lateinit var typeTV: TextView
private lateinit var descriptionTV: TextView
private lateinit var dateTV: TextView
@@ -52,60 +53,50 @@ class FurtherInfoFragment : BaseFragment<MainViewModel>(R.layout.fragment_futher
hourlyDetailHolder = view.findViewById(R.id.details_hourly_details)
unitsHolder = view.findViewById(R.id.details_units_holder)
val id = arguments!!.getLong(ID)
editButton.setOnClickListener {
navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!)
}
setupView(id)
viewModel.retrieveData(arguments)
}
private fun setupView(id: Long) {
viewModel.getCurrentShift(id)?.run {
typeTV.text = type
descriptionTV.text = description
dateTV.text = date
payRateTV.text = rateOfPay.toString()
totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString()
override fun onSuccess(data: Any?) {
super.onSuccess(data)
if (data is ShiftObject) data.setupView()
}
when (ShiftType.getEnumByType(type)) {
ShiftType.HOURLY -> {
hourlyDetailHolder.show()
unitsHolder.hide()
times.text = StringBuilder(timeIn).append("-").append(timeOut).toString()
breakTV.text = StringBuilder(breakMins).append("mins").toString()
durationTV.text = buildDurationSummary(this)
val paymentSummary =
StringBuilder().append(duration).append(" Hours @ ").append(CURRENCY)
.append(rateOfPay).append(" per Hour").append("\n")
.append("Equals: ").append(CURRENCY).append(totalPay)
totalPayTV.text = paymentSummary
}
private fun ShiftObject.setupView() {
typeTV.text = type
descriptionTV.text = description
dateTV.text = date
payRateTV.text = rateOfPay.toString()
totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString()
ShiftType.PIECE -> {
hourlyDetailHolder.hide()
unitsHolder.show()
unitsTV.text = units.toString()
when (ShiftType.getEnumByType(type)) {
ShiftType.HOURLY -> {
hourlyDetailHolder.show()
unitsHolder.hide()
times.text = StringBuilder(timeIn).append("-").append(timeOut).toString()
breakTV.text = StringBuilder().append(breakMins).append(" mins").toString()
durationTV.text = viewModel.buildDurationSummary(this)
val paymentSummary =
StringBuilder().append(duration).append(" Hours @ ")
.append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n")
.append("Equals: ").append(totalPay.formatAsCurrencyString())
totalPayTV.text = paymentSummary
}
val paymentSummary =
StringBuilder().append(units).append(" Units @ ").append(CURRENCY)
.append(rateOfPay).append(" per Unit").append("\n")
.append("Equals: ").append(CURRENCY).append(totalPay)
totalPayTV.text = paymentSummary
}
ShiftType.PIECE -> {
hourlyDetailHolder.hide()
unitsHolder.show()
unitsTV.text = units.toString()
val paymentSummary =
StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ")
.append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n")
.append("Equals: ").append(totalPay.formatAsCurrencyString())
totalPayTV.text = paymentSummary
}
}
}
private fun buildDurationSummary(shiftObject: ShiftObject): String {
val time = shiftObject.getHoursMinutesPairFromDuration()
val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second)
.append(" Minutes ")
if (shiftObject.breakMins > 0) {
stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)")
}
return stringBuilder.toString()
}
}

View File

@@ -19,7 +19,7 @@ import com.appttude.h_mal.farmr.utils.popBackStack
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
import kotlin.system.exitProcess
class MainActivity : BaseActivity<MainViewModel>() {
class MainActivity : BaseActivity() {
private lateinit var toolbar: Toolbar
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,8 +1,10 @@
package com.appttude.h_mal.farmr.utils
import java.io.IOException
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Currency
import java.util.Date
import java.util.Locale
@@ -16,6 +18,14 @@ fun Float.formatToTwoDp(): Float {
return formattedString.toFloat()
}
fun Float.formatAsCurrencyString(): String? {
val format: NumberFormat = NumberFormat.getCurrencyInstance()
format.maximumFractionDigits = 2
format.currency = Currency.getInstance("GBP")
return format.format(this)
}
fun Float.formatToTwoDpString(): String {
return formatToTwoDp().toString()
}

View File

@@ -14,6 +14,9 @@ class ApplicationViewModelFactory(
with(modelClass) {
return when {
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository)
isAssignableFrom(SubmissionViewModel::class.java) -> SubmissionViewModel(repository)
isAssignableFrom(InfoViewModel::class.java) -> InfoViewModel(repository)
isAssignableFrom(FilterViewModel::class.java) -> FilterViewModel(repository)
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}

View File

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

View File

@@ -0,0 +1,40 @@
package com.appttude.h_mal.farmr.viewmodel
import android.os.Bundle
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.utils.ID
class InfoViewModel(
repository: Repository
) : ShiftViewModel(repository) {
fun retrieveData(bundle: Bundle?) {
val id = bundle?.getLong(ID)
if (id == null) {
onError("Failed to retrieve shift")
return
}
val shift = getCurrentShift(id)
if (shift == null) {
onError("Failed to retrieve shift")
return
}
onSuccess(shift)
}
fun buildDurationSummary(shiftObject: ShiftObject): String {
val time = shiftObject.getHoursMinutesPairFromDuration()
val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second)
.append(" Minutes ")
if (shiftObject.breakMins > 0) {
stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)")
}
return stringBuilder.toString()
}
}

View File

@@ -1,13 +1,10 @@
package com.appttude.h_mal.farmr.viewmodel
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.os.Build
import android.os.Environment
import androidx.annotation.RequiresPermission
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.appttude.h_mal.farmr.base.BaseViewModel
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
@@ -21,24 +18,14 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
import com.appttude.h_mal.farmr.data.prefs.TIME_IN
import com.appttude.h_mal.farmr.data.prefs.TIME_OUT
import com.appttude.h_mal.farmr.data.prefs.TYPE
import com.appttude.h_mal.farmr.model.FilterStore
import com.appttude.h_mal.farmr.model.Order
import com.appttude.h_mal.farmr.model.Shift
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.model.Sortable
import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.CURRENCY
import com.appttude.h_mal.farmr.utils.calculateDuration
import com.appttude.h_mal.farmr.utils.convertDateString
import com.appttude.h_mal.farmr.utils.dateStringIsValid
import com.appttude.h_mal.farmr.utils.formatToTwoDp
import com.appttude.h_mal.farmr.utils.getTimeString
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
import com.appttude.h_mal.farmr.utils.sortedByOrder
import com.appttude.h_mal.farmr.utils.timeStringIsValid
import jxl.Workbook
import jxl.WorkbookSettings
import jxl.write.Label
@@ -46,25 +33,24 @@ import jxl.write.WritableWorkbook
import jxl.write.WriteException
import java.io.File
import java.io.IOException
import java.util.Calendar
import java.util.Locale
class MainViewModel(
private val repository: Repository
) : BaseViewModel() {
) : ShiftViewModel(repository) {
private val _shiftLiveData = MutableLiveData<List<ShiftObject>>()
val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
private var mSort: Sortable = Sortable.ID
private var mOrder: Order = Order.ASCENDING
private var mFilterStore: FilterStore? = null
private val observer = Observer<List<ShiftObject>> {
val result = it.applyFilters().sortList(mSort, mOrder)
onSuccess(result)
it?.let {
val result = it.applyFilters().sortList(mSort, mOrder)
onSuccess(result)
}
}
init {
@@ -148,8 +134,9 @@ class MainViewModel(
var countOfTypeP = 0
var totalUnits = 0f
var totalPay = 0f
val lines = _shiftLiveData.value?.size ?: 0
_shiftLiveData.value?.forEach {
var lines = 0
_shiftLiveData.value?.applyFilters()?.forEach {
lines += 1
totalDuration += it.duration
when (ShiftType.getEnumByType(it.type)) {
ShiftType.HOURLY -> countOfTypeH += 1
@@ -169,161 +156,6 @@ class MainViewModel(
)
}
fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id)
fun insertHourlyShift(
description: String,
date: String,
rateOfPay: Float,
timeIn: String?,
timeOut: String?,
breakMins: Int?,
) {
// Validate inputs from the edit texts
(description.length > 3).validateField {
onError("Description length should be longer")
return
}
date.dateStringIsValid().validateField {
onError("Date format is invalid")
return
}
(rateOfPay >= 0.00).validateField {
onError("Rate of pay is invalid")
return
}
timeIn?.timeStringIsValid()?.validateField {
onError("Time in format is in correct")
return
}
timeOut?.timeStringIsValid()?.validateField {
onError("Time out format is in correct")
return
}
breakMins?.let { it > 0 }?.validateField {
onError("Break in minutes is invalid")
return
}
doTry {
val result = insertShiftIntoDatabase(
ShiftType.HOURLY,
description,
date,
rateOfPay.formatToTwoDp(),
timeIn,
timeOut,
breakMins,
null
)
if (result) onSuccess(Success("Shift successfully added"))
}
}
fun insertPieceRateShift(
description: String,
date: String,
units: Float,
rateOfPay: Float
) {
// Validate inputs from the edit texts
(description.length > 3).validateField {
onError("Description length should be longer")
return
}
date.dateStringIsValid().validateField {
onError("Date format is invalid")
return
}
(rateOfPay >= 0.00).validateField {
onError("Rate of pay is invalid")
return
}
(units.toInt() >= 0).validateField {
onError("Units cannot be below zero")
return
}
doTry {
val result = insertShiftIntoDatabase(
type = ShiftType.PIECE,
description = description,
date = date,
rateOfPay = rateOfPay.formatToTwoDp(),
null,
null,
null,
units = units
)
if (result) onSuccess(Success("New shift successfully added"))
}
}
fun updateShift(
id: Long,
type: String? = null,
description: String? = null,
date: String? = null,
rateOfPay: Float? = null,
timeIn: String? = null,
timeOut: String? = null,
breakMins: Int? = null,
units: Float? = null,
) {
description?.let {
(it.length > 3).validateField {
onError("Description length should be longer")
return
}
}
date?.dateStringIsValid()?.validateField {
onError("Date format is invalid")
return
}
rateOfPay?.let {
(it >= 0.00).validateField {
onError("Rate of pay is invalid")
return
}
}
units?.let {
(it.toInt() >= 0).validateField {
onError("Units cannot be below zero")
return
}
}
timeIn?.timeStringIsValid()?.validateField {
onError("Time in format is in correct")
return
}
timeOut?.timeStringIsValid()?.validateField {
onError("Time out format is in correct")
return
}
breakMins?.let { it >= 0 }?.validateField {
onError("Break in minutes is invalid")
return
}
doTry {
val result = updateShiftInDatabase(
id,
type = type?.let { ShiftType.getEnumByType(it) },
description = description,
date = date,
rateOfPay = rateOfPay,
timeIn = timeIn,
timeOut = timeOut,
breakMins = breakMins,
units = units
)
if (result) onSuccess(Success("Shift successfully updated"))
}
}
fun deleteShift(id: Long) {
if (!repository.deleteSingleShiftFromDatabase(id)) {
onError("Failed to delete shift")
@@ -340,134 +172,6 @@ class MainViewModel(
}
}
private fun updateShiftInDatabase(
id: Long,
type: ShiftType? = null,
description: String? = null,
date: String? = null,
rateOfPay: Float? = null,
timeIn: String? = null,
timeOut: String? = null,
breakMins: Int? = null,
units: Float? = null,
): Boolean {
val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift()
?: throw IOException("Cannot update shift as it does not exist")
val shift = when (type) {
ShiftType.HOURLY -> {
// Shift type has changed so mandatory fields for hourly shift are now required as well
val insertTimeIn =
(timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted")
val insertTimeOut =
(timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
timeIn = insertTimeIn,
timeOut = insertTimeOut,
breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
)
}
ShiftType.PIECE -> {
// Shift type has changed so mandatory fields for piece rate shift are now required as well
val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
units = insertUnits,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
)
}
else -> {
if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) {
// Updates to description or date field
currentShift.copy(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
)
} else {
// Updating shifts where shift type has remained the same
when (currentShift.type) {
ShiftType.HOURLY -> {
val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException(
"No time in inserted"
)
val insertTimeOut = (timeOut ?: currentShift.timeOut)
?: throw IOException("No time out inserted")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
timeIn = insertTimeIn,
timeOut = insertTimeOut,
breakMins = breakMins ?: currentShift.breakMins,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
)
}
ShiftType.PIECE -> {
val insertUnits = (units ?: currentShift.units)
?: throw IOException("Units must be inserted for piece rate shifts")
Shift(
description = description ?: currentShift.description,
date = date ?: currentShift.date,
units = insertUnits,
rateOfPay = rateOfPay ?: currentShift.rateOfPay
)
}
}
}
}
}
return repository.updateShiftIntoDatabase(id, shift)
}
private fun insertShiftIntoDatabase(
type: ShiftType,
description: String,
date: String,
rateOfPay: Float,
timeIn: String?,
timeOut: String?,
breakMins: Int?,
units: Float?,
): Boolean {
val shift = when (type) {
ShiftType.HOURLY -> {
if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null")
val calendar by lazy { Calendar.getInstance() }
val insertTimeIn = timeIn ?: calendar.getTimeString()
val insertTimeOut = timeOut ?: calendar.getTimeString()
Shift(
description = description,
date = date,
timeIn = insertTimeIn,
timeOut = insertTimeOut,
breakMins = breakMins,
rateOfPay = rateOfPay
)
}
ShiftType.PIECE -> {
Shift(
description = description,
date = date,
units = units!!,
rateOfPay = rateOfPay,
)
}
}
return repository.insertShiftIntoDatabase(shift)
}
private fun buildInfoString(
totalDuration: Float,
countOfHourly: Int,
@@ -488,63 +192,21 @@ class MainViewModel(
stringBuilder.append("Total Units: ").append(totalUnits).append("\n")
}
if (totalPay != 0f) {
stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n")
stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString())
}
return stringBuilder.toString()
}
fun refreshLiveData() {
_shiftLiveData.postValue(repository.readShiftsFromDatabase())
repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) }
}
private inline fun Boolean.validateField(failureCallback: () -> Unit) {
if (!this) failureCallback.invoke()
}
/**
* Lambda function that will invoke onError(...) on failure
* but update live data when successful
*/
private inline fun doTry(operation: () -> Unit) {
try {
operation.invoke()
refreshLiveData()
} catch (e: Exception) {
onError(e)
}
}
fun setFiltrationDetails(
description: String?,
dateFrom: String?,
dateTo: String?,
type: String?
) {
repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type)
onSuccess(Success("Filter(s) successfully applied"))
fun clearFilters() {
super.setFiltrationDetails(null, null, null, null)
onSuccess(Success("Filters have been cleared"))
refreshLiveData()
}
fun getFiltrationDetails(): FilterStore {
val prefs = repository.retrieveFilteringDetailsInPrefs()
mFilterStore = FilterStore(
prefs[DESCRIPTION],
prefs[TIME_IN],
prefs[TIME_OUT],
prefs[TYPE]
)
return mFilterStore!!
}
fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? {
try {
return calculateDuration(mTimeIn, mTimeOut, mBreaks)
} catch (e: IOException) {
onError(e)
}
return null
}
@RequiresPermission(WRITE_EXTERNAL_STORAGE)
fun createExcelSheet(file: File): File? {
val wbSettings = WorkbookSettings().apply {
@@ -574,7 +236,8 @@ class MainViewModel(
return null
}
val sortAndOrder = getSortAndOrder()
val data = shiftLiveData.value!!.applyFilters().sortList(sortAndOrder.first, sortAndOrder.second)
val data = shiftLiveData.value!!.applyFilters()
.sortList(sortAndOrder.first, sortAndOrder.second)
var currentRow = 0
val cells = data.mapIndexed { index, shift ->
currentRow += 1

View File

@@ -0,0 +1,52 @@
package com.appttude.h_mal.farmr.viewmodel
import com.appttude.h_mal.farmr.base.BaseViewModel
import com.appttude.h_mal.farmr.data.Repository
import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
import com.appttude.h_mal.farmr.data.prefs.DATE_IN
import com.appttude.h_mal.farmr.data.prefs.DATE_OUT
import com.appttude.h_mal.farmr.data.prefs.TYPE
import com.appttude.h_mal.farmr.model.FilterStore
open class ShiftViewModel(
private val repository: Repository
) : BaseViewModel() {
/*
* Add Item & Further info
*/
fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id)
/**
* Lambda function that will invoke onError(...) on failure
* but update live data when successful
*/
private inline fun doTry(operation: () -> Unit) {
try {
operation.invoke()
} catch (e: Exception) {
onError(e)
}
}
open fun setFiltrationDetails(
description: String?,
dateFrom: String?,
dateTo: String?,
type: String?
) {
repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type)
}
open fun getFiltrationDetails(): FilterStore {
val prefs = repository.retrieveFilteringDetailsInPrefs()
return FilterStore(
prefs[DESCRIPTION],
prefs[DATE_IN],
prefs[DATE_OUT],
prefs[TYPE]
)
}
}

View File

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