diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d99..b1077fb 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8a92332..dafe122 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,6 @@ dependencies { 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' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' @@ -48,4 +46,7 @@ dependencies { def kodein_version = "6.2.1" implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" + / * jxl * / + implementation 'net.sourceforge.jexcelapi:jxl:2.6.12' + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 12b8ee2..5b13a45 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,6 +37,16 @@ android:name="com.appttude.h_mal.farmr.data.legacydb.ShiftProvider" android:authorities="com.appttude.h_mal.farmr" android:exported="false" /> + + + + \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt index affb4b0..a939028 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.createViewModelLazy 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 @@ -76,4 +77,6 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) fun setTitle(title: String) { (requireActivity() as BaseActivity<*>).setTitleInActionBar(title) } + + fun popBackStack() = mActivity?.popBackStack() } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt index ab62a76..fd34388 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt @@ -13,11 +13,6 @@ open class BaseRecyclerAdapter( ): RecyclerView.Adapter() { var list: List? = null - fun updateData(newList: List) { - list = newList - notifyDataSetChanged() - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return if (list.isNullOrEmpty()) { val emptyViewHolder = parent.generateView(emptyViewId) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 3d8e5ab..2f87159 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -39,7 +39,7 @@ data class Shift( timeOut, duration, breakTime, - duration, + 0f, rateOfPay, (duration * rateOfPay).formatToTwoDp() ) diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt index b724c19..4087686 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt @@ -4,10 +4,6 @@ enum class ShiftType(val type: String){ HOURLY("Hourly"), PIECE("Piece Rate"); - fun getEnumByType(type: String): ShiftType { - return values().first { it.type == type } - } - companion object { fun getEnumByType(type: String): ShiftType { return values().first { it.type == type } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt index 6a0c9a1..f027a92 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -7,5 +7,9 @@ enum class Sortable(val label: String) { DESCRIPTION("Description"), DURATION("Added"), UNITS("Duration"), RATEOFPAY("Rate of pay"), - TOTALPAY("Total Pay") + TOTALPAY("Total Pay"); + + companion object { + val entries = Sortable.values() + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt new file mode 100644 index 0000000..b7b3d9d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.model + +data class Success( + val successMessage: String +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt index ca917f3..745fb68 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -12,6 +12,7 @@ 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.MainViewModel @@ -32,7 +33,6 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ 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) @@ -74,8 +74,8 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ id: Long ) { type = when (position) { - 1 -> ShiftType.HOURLY.toString() - 2 -> ShiftType.PIECE.toString() + 1 -> ShiftType.HOURLY.type + 2 -> ShiftType.PIECE.type else -> return } } @@ -89,4 +89,9 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ override fun onClick(p0: View?) { submitFiltrationDetails() } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) popBackStack() + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index 1454eb8..eb74f10 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -14,8 +14,10 @@ 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.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.popBackStack @@ -123,6 +125,13 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), 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 @@ -132,16 +141,26 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), 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.formatToTwoDpString()) mTotalPayTextView.text = totalPay.formatToTwoDpString() + + calculateTotalPay() } // Return title @@ -268,4 +287,11 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), return true } + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) { + displayToast(data.successMessage) + popBackStack() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index b4f557b..f083778 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -6,31 +6,41 @@ import android.app.Activity import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.appttude.h_mal.farmr.R import com.appttude.h_mal.farmr.base.BackPressedListener import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.Order import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.viewmodel.MainViewModel import com.google.android.material.floatingactionbutton.FloatingActionButton +import java.io.File import kotlin.system.exitProcess + class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { - lateinit var activity: MainActivity 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) } @@ -43,6 +53,15 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } productListView = view.findViewById(R.id.list_item_view) productListView.adapter = mAdapter + emptyView = view.findViewById(R.id.empty_view) + + mAdapter.registerAdapterDataObserver(object : AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + if (mAdapter.itemCount == 0) emptyView.show() + else emptyView.hide() + } + }) view.findViewById(R.id.fab1).setOnClickListener { navigateToFragment(FragmentAddItem(), name = "additem") @@ -58,8 +77,12 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr override fun onSuccess(data: Any?) { super.onSuccess(data) if (data is List<*>) { + @Suppress("UNCHECKED_CAST") mAdapter.submitList(data as List) } + if (data is Success) { + displayToast(data.successMessage) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -79,12 +102,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } R.id.filter_data -> { -// val fragmentTransaction: FragmentTransaction = -// activity.fragmentManager!!.beginTransaction() -// fragmentTransaction.replace(R.id.container, FilterDataFragment()) -// .addToBackStack("filterdata").commit() - // Todo: filter shift - + navigateToFragment(FilterDataFragment(), name = "filterdata") return true } @@ -92,8 +110,9 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr sortData() return true } + R.id.clear_filter -> { - // Todo: Apply filter to list + viewModel.setFiltrationDetails(null, null, null, null) return true } @@ -103,7 +122,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() } + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() } .create().show() } else { Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT) @@ -159,157 +178,29 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr ) } - private fun ExportData() { -// val permission = -// ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) -// if (permission != PackageManager.PERMISSION_GRANTED) { -// Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() -// return -// } -// shiftsDbhelper = ShiftsDbHelper(context) -// val database = shiftsDbhelper!!.writableDatabase -// val projection_export = arrayOf( -// ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, -// ShiftsEntry.COLUMN_SHIFT_DATE, -// ShiftsEntry.COLUMN_SHIFT_TIME_IN, -// ShiftsEntry.COLUMN_SHIFT_TIME_OUT, -// ShiftsEntry.COLUMN_SHIFT_BREAK, -// ShiftsEntry.COLUMN_SHIFT_DURATION, -// ShiftsEntry.COLUMN_SHIFT_TYPE, -// ShiftsEntry.COLUMN_SHIFT_UNIT, -// ShiftsEntry.COLUMN_SHIFT_PAYRATE, -// ShiftsEntry.COLUMN_SHIFT_TOTALPAY -// ) -// val cursor = activity.contentResolver.query( -// ShiftsEntry.CONTENT_URI, -// projection_export, -// activity.selection, -// activity.args, -// activity.sortOrder -// ) -// database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null) -// var totalDuration = 0.00f -// var totalUnits = 0.00f -// var totalPay = 0.00f -// try { -// while (cursor!!.moveToNext()) { -// val descriptionColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) -// val dateColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) -// val timeInColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) -// val timeOutColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) -// val durationColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) -// val breakOutColumnIndex = -// cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK)) -// val typeColumnIndex = -// cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) -// val unitColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) -// val payrateColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE)) -// val totalpayColumnIndex = -// cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) -// totalUnits = totalUnits + unitColumnIndex -// totalDuration = totalDuration + durationColumnIndex -// totalPay = totalPay + totalpayColumnIndex -// val values = ContentValues() -// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex) -// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex) -// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) -// } -// } catch (e: Exception) { -// Log.e("FragmentMain", "ExportData: ", e) -// } finally { -// val values = ContentValues() -// values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "") -// values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:") -// values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration) -// values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:") -// values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits) -// values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:") -// values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) -// database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) -// cursor!!.close() -// } -// val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp" -// val file = File(savePath) -// if (!file.exists()) { -// file.mkdirs() -// } -// val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath) -// sqLiteToExcel.exportSingleTable( -// "shiftsexport", -// "shifthistory.xls", -// object : ExportListener { -// override fun onStart() {} -// override fun onCompleted(filePath: String) { -// Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show() -// val newPath = Uri.parse("file://$savePath/shifthistory.xls") -// val builder = VmPolicy.Builder() -// StrictMode.setVmPolicy(builder.build()) -// val emailintent = Intent(Intent.ACTION_SEND) -// emailintent.type = "application/vnd.ms-excel" -// emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts") -// emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.") -// emailintent.putExtra(Intent.EXTRA_STREAM, newPath) -// startActivity(Intent.createChooser(emailintent, "Send Email")) -// } -// -// override fun onError(e: Exception) { -// println("Error msg: $e") -// Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show() -// } -// }) - } + private fun 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 + } - @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)" + 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) } - 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 onRequestPermissionsResult( @@ -335,7 +226,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setTitle("Export?") .setMessage("Exporting current filtered data. Continue?") .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() + .setPositiveButton(android.R.string.yes) { arg0, arg1 -> exportData() }.create().show() } fun checkStoragePermissions(activity: Activity?): Boolean { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt index 7be48db..00f9119 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -3,6 +3,7 @@ package com.appttude.h_mal.farmr.utils import java.io.IOException import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Date import java.util.Locale fun String.formatToTwoDp(): Float { @@ -32,6 +33,11 @@ fun Calendar.getTimeString(): String { 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 * diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index 08d1185..bec425b 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -1,11 +1,26 @@ 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 +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.data.prefs.DESCRIPTION import com.appttude.h_mal.farmr.data.prefs.TIME_IN import com.appttude.h_mal.farmr.data.prefs.TIME_OUT @@ -15,14 +30,24 @@ 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.sortedByOrder import com.appttude.h_mal.farmr.utils.timeStringIsValid +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.Calendar +import java.util.Locale class MainViewModel( @@ -38,7 +63,8 @@ class MainViewModel( private var mFilterStore: FilterStore? = null private val observer = Observer> { - onSuccess(it.sortList(mSort, mOrder)) + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) } init { @@ -47,6 +73,47 @@ class MainViewModel( shiftLiveData.observeForever(observer) } + private fun List.applyFilters(): List { + val filter = getFiltrationDetails() + + return filter { s -> + comparedStrings(filter.type, s.type) && + comparedStringsContains(filter.description, s.description) && + (isBetween(filter.dateFrom, filter.dateTo, s.date) ?: true) + } + } + + private fun comparedStrings(first: String?, second: String?): Boolean { + return when (compareValues(first, second)) { + -1, 0, 1 -> true + else -> { + false + } + } + } + + private fun comparedStringsContains(first: String?, second: String?): Boolean { + first?.let { + (second?.contains(it))?.let { c -> return c } + } + + return comparedStrings(first, second) + } + + private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? { + val first = fromDate?.convertDateString() + val second = toDate?.convertDateString() + + if (first == null && second == null) return null + val compareDate = compareWith.convertDateString() ?: return null + + if (second == null) return compareDate.after(first) + if (first == null) return compareDate.before(second) + + return compareDate.after(first) && compareDate.before(second) + } + + override fun onCleared() { shiftLiveData.removeObserver(observer) super.onCleared() @@ -121,7 +188,7 @@ class MainViewModel( onError("Date format is invalid") return } - (rateOfPay.toFloat() >= 0.00).validateField { + (rateOfPay >= 0.00).validateField { onError("Rate of pay is invalid") return } @@ -139,7 +206,7 @@ class MainViewModel( } doTry { - insertShiftIntoDatabase( + val result = insertShiftIntoDatabase( ShiftType.HOURLY, description, date, @@ -149,6 +216,8 @@ class MainViewModel( breakMins, null ) + + if (result) onSuccess(Success("Shift successfully added")) } } @@ -178,7 +247,7 @@ class MainViewModel( } doTry { - insertShiftIntoDatabase( + val result = insertShiftIntoDatabase( type = ShiftType.PIECE, description = description, date = date, @@ -188,6 +257,7 @@ class MainViewModel( null, units = units ) + if (result) onSuccess(Success("New shift successfully added")) } } @@ -203,7 +273,7 @@ class MainViewModel( units: Float? = null, ) { description?.let { - (it.length < 3).validateField { + (it.length > 3).validateField { onError("Description length should be longer") return } @@ -213,7 +283,7 @@ class MainViewModel( return } rateOfPay?.let { - (it.toFloat() >= 0.00).validateField { + (it >= 0.00).validateField { onError("Rate of pay is invalid") return } @@ -232,13 +302,13 @@ class MainViewModel( onError("Time out format is in correct") return } - breakMins?.let { it.toInt() > 0 }?.validateField { + breakMins?.let { it >= 0 }?.validateField { onError("Break in minutes is invalid") return } doTry { - updateShiftInDatabase( + val result = updateShiftInDatabase( id, type = type?.let { ShiftType.getEnumByType(it) }, description = description, @@ -249,6 +319,8 @@ class MainViewModel( breakMins = breakMins, units = units ) + + if (result) onSuccess(Success("Shift successfully updated")) } } @@ -278,7 +350,7 @@ class MainViewModel( 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") @@ -352,7 +424,7 @@ class MainViewModel( } } - repository.updateShiftIntoDatabase(id, shift) + return repository.updateShiftIntoDatabase(id, shift) } private fun insertShiftIntoDatabase( @@ -364,7 +436,7 @@ class MainViewModel( 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") @@ -392,7 +464,7 @@ class MainViewModel( } } - repository.insertShiftIntoDatabase(shift) + return repository.insertShiftIntoDatabase(shift) } @@ -404,30 +476,21 @@ class MainViewModel( totalPay: Float, lines: Int ): String { - var textString: String - textString = "$lines Shifts" + val stringBuilder = StringBuilder("$lines Shifts").append("\n") + if (countOfHourly != 0 && countOfPiece != 0) { - textString = "$textString ($countOfHourly Hourly/$countOfPiece Piece Rate)" + stringBuilder.append(" ($countOfHourly Hourly/$countOfPiece Piece Rate)").append("\n") } if (countOfHourly != 0) { - textString = """ - $textString - Total Hours: ${String.format("%.2f", totalDuration)} - """.trimIndent() + stringBuilder.append("Total Hours: ").append(totalDuration).append("\n") } if (countOfPiece != 0) { - textString = """ - $textString - Total Units: ${String.format("%.2f", totalUnits)} - """.trimIndent() + stringBuilder.append("Total Units: ").append(totalUnits).append("\n") } if (totalPay != 0f) { - textString = """ - $textString - Total Pay: ${"$"}${String.format("%.2f", totalPay)} - """.trimIndent() + stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n") } - return textString + return stringBuilder.toString() } fun refreshLiveData() { @@ -458,11 +521,13 @@ class MainViewModel( type: String? ) { repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + onSuccess(Success("Filter(s) successfully applied")) + refreshLiveData() } fun getFiltrationDetails(): FilterStore { val prefs = repository.retrieveFilteringDetailsInPrefs() - mFilterStore = FilterStore( + mFilterStore = FilterStore( prefs[DESCRIPTION], prefs[TIME_IN], prefs[TIME_OUT], @@ -473,11 +538,90 @@ class MainViewModel( fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { try { - return calculateDuration(mTimeIn,mTimeOut,mBreaks) - }catch (e: IOException) { + 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 { + 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.mapIndexed { index, shift -> + currentRow += 1 + listOf( + Label(0, currentRow, shift.id.toString()), + Label(1, currentRow, shift.type), + Label(2, currentRow, shift.description), + Label(3, currentRow, shift.date), + Label(4, currentRow, shift.timeIn), + Label(5, currentRow, shift.timeOut), + Label(6, currentRow, shift.breakMins.toString()), + Label(7, currentRow, shift.duration.toString()), + Label(8, currentRow, shift.units.toString()), + Label(9, currentRow, shift.rateOfPay.toString()), + Label(10, currentRow, shift.totalPay.toString()) + ) + }.flatten() + + currentRow += 1 + val footer = listOf( + Label(0, currentRow, "Total:"), + Label(7, currentRow, data.sumOf { it.duration.toDouble() }.toString()), + Label(8, currentRow, data.sumOf { it.units.toDouble() }.toString()), + Label(10, currentRow, data.sumOf { it.totalPay.toDouble() }.toString()) + ) + val content = listOf(headers, cells, footer).flatten() + + // Write content to sheet + try { + content.forEach { c -> sheet.addCell(c) } + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } + + workbook.write() + workbook.close() + + return file + } catch (e: IOException) { + e.printStackTrace() + onError("Failed to generate excel sheet of shifts") + } + return null + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/farm.jpg b/app/src/main/res/drawable/farm.jpg deleted file mode 100644 index 290da71..0000000 Binary files a/app/src/main/res/drawable/farm.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml deleted file mode 100644 index 34b8202..0000000 --- a/app/src/main/res/drawable/ic_info_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_release_background.xml b/app/src/main/res/drawable/ic_launcher_release_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_release_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml deleted file mode 100644 index e3400cf..0000000 --- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml deleted file mode 100644 index 2aef437..0000000 --- a/app/src/main/res/drawable/ic_sync_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/img_i_full.png b/app/src/main/res/drawable/img_i_full.png deleted file mode 100644 index 97c195c..0000000 Binary files a/app/src/main/res/drawable/img_i_full.png and /dev/null differ diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index dfa3dfc..0000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 681d46f..e9fd9c1 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -23,4 +23,9 @@ android:src="@drawable/add" app:backgroundTint="@color/colorPrimary" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 077fa23..7120641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,9 @@ Farmr - Settings Shifts Add Shift Edit Shift - Delete Shift failed to insert Shift Shift successfully added Update Failed @@ -25,11 +23,6 @@ General - Enable social recommendations - Recommendations for people to contact - based on your message history - - Display name John Smith diff --git a/app/src/main/res/xml/provider_path.xml b/app/src/main/res/xml/provider_path.xml new file mode 100644 index 0000000..4495c28 --- /dev/null +++ b/app/src/main/res/xml/provider_path.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index f56abf1..4e3983e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.7.10' } repositories { google()