From d95742dd9f80506a0d773cbba7bc6a4508de1964 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Thu, 31 Mar 2022 22:00:12 -0600 Subject: Added support for sharing and saving reports --- .../trackermap/TrackerMap/utils/NSDataByteArray.kt | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/NSDataByteArray.kt (limited to 'shared/src') diff --git a/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/NSDataByteArray.kt b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/NSDataByteArray.kt new file mode 100644 index 0000000..807c63f --- /dev/null +++ b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/NSDataByteArray.kt @@ -0,0 +1,40 @@ +/** + * TrackerMap + * Copyright (C) 2021-2022 Iván Ávalos , Henoch Ojeda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package mx.trackermap.TrackerMap.utils + +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.create +import platform.posix.memcpy + +// Source: https://gist.github.com/noahsark769/61cfb7a8b7231e2069a9dab94cf74a62 + +fun ByteArray.toData(): NSData = memScoped { + NSData.create( + bytes = allocArrayOf(this@toData), + length = this@toData.size.toULong()) +} + +fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) + } +} \ No newline at end of file -- cgit v1.2.3 From a6a95167dbfe48fa0c5d6b61219309d346142979 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Sat, 9 Apr 2022 00:02:03 -0500 Subject: - [shared] Rewrote ReportDates to allow for custom date period. - [android] Rewrote report handling to use new ReportPeriod. - [ios] Not rewritten yet, it won't build! --- .../details/information/UnitInformationFragment.kt | 2 +- .../android/details/reports/UnitReportsFragment.kt | 34 ++--- .../src/main/res/menu/report_period_options.xml | 4 + androidApp/src/main/res/values-es-rMX/strings.xml | 1 + androidApp/src/main/res/values/strings.xml | 1 + .../TrackerMap/controllers/ReportController.kt | 2 +- .../mx/trackermap/TrackerMap/utils/ReportDates.kt | 153 ++++++++++++--------- 7 files changed, 116 insertions(+), 81 deletions(-) (limited to 'shared/src') diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/information/UnitInformationFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/information/UnitInformationFragment.kt index e731587..fb0a0bc 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/information/UnitInformationFragment.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/information/UnitInformationFragment.kt @@ -115,7 +115,7 @@ class UnitInformationFragment : Fragment() { } private fun displayInformation(unit: UnitInformation) { - val context = context!! + val context = requireContext() val details: MutableList> = mutableListOf() unit.device.contact?.let { contact -> details.add(getString(R.string.unit_info_contact) to contact) diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt index 60ad531..091cb7b 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt @@ -137,7 +137,7 @@ class UnitReportsFragment : Fragment() { exportAction = UnitReportsViewModel.ExportAction.ACTION_SHARE unitReportsViewModel.fetchReportXlsx() } - unitReportsViewModel.setReportPeriod(ReportDates.ReportPeriod.TODAY) + unitReportsViewModel.setReportPeriod(ReportDates.ReportPeriod.Today()) unitReportsViewModel.setReportType(ReportController.ReportType.POSITIONS) } @@ -181,13 +181,14 @@ class UnitReportsFragment : Fragment() { binding.periodButton.text = context?.getString( when (period) { - ReportDates.ReportPeriod.TODAY -> R.string.period_today - ReportDates.ReportPeriod.LAST_24 -> R.string.period_last_24 - ReportDates.ReportPeriod.YESTERDAY -> R.string.period_yesterday - ReportDates.ReportPeriod.THIS_WEEK -> R.string.period_this_week - ReportDates.ReportPeriod.LAST_7 -> R.string.period_last_7 - ReportDates.ReportPeriod.THIS_MONTH -> R.string.period_this_month - ReportDates.ReportPeriod.LAST_30 -> R.string.period_last_30 + is ReportDates.ReportPeriod.Today -> R.string.period_today + is ReportDates.ReportPeriod.Last24 -> R.string.period_last_24 + is ReportDates.ReportPeriod.Yesterday -> R.string.period_yesterday + is ReportDates.ReportPeriod.ThisWeek -> R.string.period_this_week + is ReportDates.ReportPeriod.Last7 -> R.string.period_last_7 + is ReportDates.ReportPeriod.ThisMonth -> R.string.period_this_month + is ReportDates.ReportPeriod.Last30 -> R.string.period_last_30 + is ReportDates.ReportPeriod.Custom -> R.string.period_custom } ) } @@ -210,14 +211,15 @@ class UnitReportsFragment : Fragment() { popOver.setOnMenuItemClickListener { item -> unitReportsViewModel.setReportPeriod( when (item.itemId) { - R.id.optionToday -> ReportDates.ReportPeriod.TODAY - R.id.optionLast24 -> ReportDates.ReportPeriod.LAST_24 - R.id.optionYesterday -> ReportDates.ReportPeriod.YESTERDAY - R.id.optionWeek -> ReportDates.ReportPeriod.THIS_WEEK - R.id.optionLast7 -> ReportDates.ReportPeriod.LAST_7 - R.id.optionMonth -> ReportDates.ReportPeriod.THIS_MONTH - R.id.optionLast30 -> ReportDates.ReportPeriod.LAST_30 - else -> ReportDates.ReportPeriod.TODAY + R.id.optionToday -> ReportDates.ReportPeriod.Today() + R.id.optionLast24 -> ReportDates.ReportPeriod.Last24() + R.id.optionYesterday -> ReportDates.ReportPeriod.Yesterday() + R.id.optionWeek -> ReportDates.ReportPeriod.ThisWeek() + R.id.optionLast7 -> ReportDates.ReportPeriod.Last7() + R.id.optionMonth -> ReportDates.ReportPeriod.ThisMonth() + R.id.optionLast30 -> ReportDates.ReportPeriod.Last30() + R.id.optionCustom -> ReportDates.ReportPeriod.Custom() + else -> ReportDates.ReportPeriod.Today() } ) true diff --git a/androidApp/src/main/res/menu/report_period_options.xml b/androidApp/src/main/res/menu/report_period_options.xml index 60fb58c..a8fdbad 100644 --- a/androidApp/src/main/res/menu/report_period_options.xml +++ b/androidApp/src/main/res/menu/report_period_options.xml @@ -30,4 +30,8 @@ android:id="@+id/optionLast30" android:title="@string/period_last_30" /> + + \ No newline at end of file diff --git a/androidApp/src/main/res/values-es-rMX/strings.xml b/androidApp/src/main/res/values-es-rMX/strings.xml index 8792b08..d07b18d 100644 --- a/androidApp/src/main/res/values-es-rMX/strings.xml +++ b/androidApp/src/main/res/values-es-rMX/strings.xml @@ -112,6 +112,7 @@ Últimos 7d Mes Últimos 30d + Personalizado Evento Fecha y hora diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index a6cba19..ce07188 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -126,6 +126,7 @@ Last 7d Month Last 30d + Custom Event Datetime diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt index 29c4229..e120e97 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt @@ -60,7 +60,7 @@ class ReportController( return } - val (currentDate, previousDate) = ReportDates.getDates(reportPeriod) + val (currentDate, previousDate) = reportPeriod.getDates() if (!xlsx) { reportFlow.value = Report.LoadingReport diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt index 5672536..64df79d 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt @@ -22,75 +22,102 @@ import kotlinx.datetime.* @DelicateCoroutinesApi class ReportDates { - enum class ReportPeriod { - TODAY, LAST_24, YESTERDAY, THIS_WEEK, LAST_7, THIS_MONTH, LAST_30 - } + sealed class ReportPeriod { + val timezone = TimeZone.currentSystemDefault() + val clock = Clock.System + val instant = clock.now() + val dateTime = instant.toLocalDateTime(timezone) + val date = dateTime.date + + abstract fun getDates(): Pair - companion object { - private fun formatDateTime(dateTime: LocalDateTime, timezone: TimeZone) = + fun formatDateTime(dateTime: LocalDateTime) = dateTime.toInstant(timezone).toString() - fun getDates(period: ReportPeriod): Pair { - val timezone = TimeZone.currentSystemDefault() - val clock = Clock.System - val instant = clock.now() - val dateTime = instant.toLocalDateTime(timezone) - val date = dateTime.date + class Today : ReportPeriod() { + override fun getDates(): Pair { + val from = date.atTime(0, 0) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) + } + } - var currentDate: LocalDateTime? = null - var previousDate: LocalDateTime? = null - when (period) { - ReportPeriod.TODAY -> { - previousDate = date.atTime(0, 0) - currentDate = dateTime - } - ReportPeriod.LAST_24 -> { - previousDate = instant - .minus(1, DateTimeUnit.DAY, timezone) - .toLocalDateTime(timezone) - currentDate = dateTime - } - ReportPeriod.YESTERDAY -> { - val yesterday = instant - .minus(1, DateTimeUnit.DAY, timezone) - .toLocalDateTime(timezone).date - previousDate = yesterday.atTime(0, 0) - currentDate = yesterday.atTime(23, 59) - } - ReportPeriod.THIS_WEEK -> { - previousDate = instant - .minus(date.dayOfWeek.isoDayNumber - 1, DateTimeUnit.DAY, timezone) - .toLocalDateTime(timezone).date - .atTime(0, 0) - currentDate = dateTime - } - ReportPeriod.LAST_7 -> { - previousDate = instant - .minus(1, DateTimeUnit.WEEK, timezone) - .toLocalDateTime(timezone).date - .atTime(0, 0) - currentDate = dateTime - } - ReportPeriod.THIS_MONTH -> { - previousDate = instant - .minus(date.dayOfMonth - 1, DateTimeUnit.DAY, timezone) - .toLocalDateTime(timezone).date - .atTime(0, 0) - currentDate = dateTime - } - ReportPeriod.LAST_30 -> { - previousDate = instant - .minus(1, DateTimeUnit.MONTH, timezone) - .toLocalDateTime(timezone).date - .atTime(0, 0) - currentDate = dateTime - } + class Last24 : ReportPeriod() { + override fun getDates(): Pair { + val from = instant + .minus(1, DateTimeUnit.DAY, timezone) + .toLocalDateTime(timezone) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) } + } + + class Yesterday : ReportPeriod() { + override fun getDates(): Pair { + val yesterday = instant + .minus(1, DateTimeUnit.DAY, timezone) + .toLocalDateTime(timezone).date + val from = yesterday.atTime(0, 0) + val to = yesterday.atTime(23, 59) + return formatDateTime(to) to formatDateTime(from) + } + } + + class ThisWeek : ReportPeriod() { + override fun getDates(): Pair { + val from = instant + .minus(date.dayOfWeek.isoDayNumber - 1, DateTimeUnit.DAY, timezone) + .toLocalDateTime(timezone).date + .atTime(0, 0) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) + } + } - return Pair( - formatDateTime(currentDate, timezone), - formatDateTime(previousDate, timezone) - ) + class Last7 : ReportPeriod() { + override fun getDates(): Pair { + val from = instant + .minus(1, DateTimeUnit.WEEK, timezone) + .toLocalDateTime(timezone).date + .atTime(0, 0) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) + } + } + + class ThisMonth : ReportPeriod() { + override fun getDates(): Pair { + val from = instant + .minus(date.dayOfMonth - 1, DateTimeUnit.DAY, timezone) + .toLocalDateTime(timezone).date + .atTime(0, 0) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) + } + } + + class Last30 : ReportPeriod() { + override fun getDates(): Pair { + val from = instant + .minus(1, DateTimeUnit.MONTH, timezone) + .toLocalDateTime(timezone).date + .atTime(0, 0) + val to = dateTime + return formatDateTime(to) to formatDateTime(from) + } + } + + class Custom ( + private val from: LocalDateTime? = null, + private val to: LocalDateTime? = null + ) : ReportPeriod() { + override fun getDates(): Pair { + return formatDateTime( + to ?: dateTime + ) to formatDateTime( + from ?: date.atTime(0, 0) + ) + } } } } \ No newline at end of file -- cgit v1.2.3 From 546738888395565ac2d5fe2cfb941ecdd0c1df45 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Mon, 11 Apr 2022 00:55:26 -0500 Subject: - [shared] Added DateUtils 'expect' class for platform-specific date format. - [android] Redesigned reports view, now supports custom reports! - [ios] Not rewritten yet, it won't build! --- androidApp/build.gradle.kts | 4 +- .../android/details/reports/UnitReportsFragment.kt | 116 +++++- androidApp/src/main/res/drawable/icon_down.xml | 10 + androidApp/src/main/res/drawable/icon_up.xml | 10 + .../src/main/res/layout/unit_details_reports.xml | 413 +++++++++++++-------- androidApp/src/main/res/values-es-rMX/strings.xml | 5 + androidApp/src/main/res/values/dimen.xml | 6 + androidApp/src/main/res/values/strings.xml | 5 + androidApp/src/main/res/values/styles.xml | 13 + .../mx/trackermap/TrackerMap/utils/DateUtils.kt | 23 ++ .../TrackerMap/controllers/ReportController.kt | 15 +- .../mx/trackermap/TrackerMap/utils/DateUtils.kt | 9 + .../mx/trackermap/TrackerMap/utils/Formatter.kt | 5 +- .../mx/trackermap/TrackerMap/utils/ReportDates.kt | 45 ++- 14 files changed, 484 insertions(+), 195 deletions(-) create mode 100644 androidApp/src/main/res/drawable/icon_down.xml create mode 100644 androidApp/src/main/res/drawable/icon_up.xml create mode 100644 shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt create mode 100644 shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt (limited to 'shared/src') diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 5bb488c..c813186 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -14,8 +14,8 @@ android { versionCode = 1 versionName = "1.0" ndk { - abiFilters.clear() - abiFilters += listOf("armeabi-v7a", "arm64-v8a") + // abiFilters.clear() + // abiFilters += listOf("armeabi-v7a", "arm64-v8a") } } buildTypes { diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt index 091cb7b..03a4220 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt @@ -18,6 +18,8 @@ package mx.trackermap.TrackerMap.android.details.reports import android.app.Activity +import android.app.DatePickerDialog +import android.app.TimePickerDialog import android.content.Intent import android.os.Build import android.os.Bundle @@ -27,14 +29,16 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.PopupMenu -import android.widget.TableRow -import android.widget.TextView +import android.widget.* import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.setMargins import androidx.fragment.app.Fragment import androidx.fragment.app.commit +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.card.MaterialCardView +import io.ktor.utils.io.* import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.datetime.LocalDateTime import mx.trackermap.TrackerMap.android.R import mx.trackermap.TrackerMap.android.databinding.UnitDetailsReportsBinding import mx.trackermap.TrackerMap.android.details.UnitDetailsAdapter @@ -47,6 +51,7 @@ import mx.trackermap.TrackerMap.utils.Formatter import mx.trackermap.TrackerMap.utils.ReportDates import org.koin.androidx.viewmodel.ext.android.viewModel import java.io.* +import java.util.* import kotlin.math.max import kotlin.time.ExperimentalTime @@ -59,12 +64,22 @@ class UnitReportsFragment : Fragment() { private val unitReportsViewModel: UnitReportsViewModel by viewModel() private lateinit var mapFragment: MapWrapperFragment + private lateinit var bottomSheetBehavior: BottomSheetBehavior private var reportFile: ReportController.Report.XlsxReport? = null private var exportAction: UnitReportsViewModel.ExportAction? = null + private lateinit var fromPickedDatetime: LocalDateTime + private lateinit var fromDatePicker: DatePickerDialog + private lateinit var fromTimePicker: TimePickerDialog + + private lateinit var toPickedDatetime: LocalDateTime + private lateinit var toDatePicker: DatePickerDialog + private lateinit var toTimePicker: TimePickerDialog + private companion object { - const val XLSX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + const val XLSX_MIME_TYPE = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } override fun onCreateView( @@ -80,7 +95,12 @@ class UnitReportsFragment : Fragment() { super.onViewCreated(view, savedInstanceState) unitReportsViewModel.setDeviceId( - arguments?.getInt(UnitDetailsAdapter.DEVICE_ID_ARG) ?: 0) + arguments?.getInt(UnitDetailsAdapter.DEVICE_ID_ARG) ?: 0 + ) + + bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomSheet) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + setupEvents() initializeMap() } @@ -129,6 +149,12 @@ class UnitReportsFragment : Fragment() { binding.periodButton.setOnClickListener { showPeriodPopUp(it) } + binding.fromEditText.setOnClickListener { + fromDatePicker.show() + } + binding.toEditText.setOnClickListener { + toDatePicker.show() + } binding.exportButton.setOnClickListener { exportAction = UnitReportsViewModel.ExportAction.ACTION_SAVE unitReportsViewModel.fetchReportXlsx() @@ -137,6 +163,26 @@ class UnitReportsFragment : Fragment() { exportAction = UnitReportsViewModel.ExportAction.ACTION_SHARE unitReportsViewModel.fetchReportXlsx() } + binding.toggleSheet.setOnClickListener { + if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + bottomSheetBehavior.addBottomSheetCallback(object: BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_EXPANDED) { + binding.toggleSheet.setImageResource(R.drawable.icon_down) + binding.toggleSheet.contentDescription = getString(R.string.shared_slide_down) + } else { + binding.toggleSheet.setImageResource(R.drawable.icon_up) + binding.toggleSheet.contentDescription = getString(R.string.shared_slide_up) + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) unitReportsViewModel.setReportPeriod(ReportDates.ReportPeriod.Today()) unitReportsViewModel.setReportType(ReportController.ReportType.POSITIONS) } @@ -149,7 +195,11 @@ class UnitReportsFragment : Fragment() { when (report) { is ReportController.Report.PositionsReport -> { mapFragment.display(unitReportsViewModel.geofences.value!!) - mapFragment.display(report.positions.toTypedArray(), isReport = true, center = true) + mapFragment.display( + report.positions.toTypedArray(), + isReport = true, + center = true + ) showMap(true) } is ReportController.Report.EventsReport -> { @@ -191,6 +241,55 @@ class UnitReportsFragment : Fragment() { is ReportDates.ReportPeriod.Custom -> R.string.period_custom } ) + + binding.rangeLayout.visibility = + if (period is ReportDates.ReportPeriod.Custom) View.VISIBLE else View.GONE + + (period as? ReportDates.ReportPeriod.Custom)?.let { + val (from, to) = it.getStringDates() + binding.fromEditText.setText(Formatter.formatDate(from)) + binding.toEditText.setText(Formatter.formatDate(to)) + } + + /** + * Initialize date and time pickers + */ + + val (from, to) = period.getObjectDates() + + fromDatePicker = DatePickerDialog(requireContext(), { p0, p1, p2, p3 -> + fromPickedDatetime = LocalDateTime(p1, p2, p3, from.hour, from.minute) + fromTimePicker.show() + }, from.year, from.monthNumber, from.dayOfMonth) + + fromTimePicker = TimePickerDialog(requireContext(), { p0, p1, p2 -> + fromPickedDatetime = LocalDateTime( + fromPickedDatetime.year, + fromPickedDatetime.monthNumber, + fromPickedDatetime.dayOfMonth, + p1, p2 + ) + (period as? ReportDates.ReportPeriod.Custom)?.let { + unitReportsViewModel.setReportPeriod(it.withFrom(fromPickedDatetime)) + } + }, from.hour, from.minute, false) + + toDatePicker = DatePickerDialog(requireContext(), { p0, p1, p2, p3 -> + toPickedDatetime = LocalDateTime(p1, p2, p3, from.hour, from.minute) + toTimePicker.show() + }, to.year, to.monthNumber, to.dayOfMonth) + + toTimePicker = TimePickerDialog(requireContext(), { p0, p1, p2 -> + toPickedDatetime = LocalDateTime( + toPickedDatetime.year, + toPickedDatetime.monthNumber, + toPickedDatetime.dayOfMonth, + p1, p2 + ) + (period as? ReportDates.ReportPeriod.Custom)?.let { + unitReportsViewModel.setReportPeriod(it.withFrom(fromPickedDatetime)) + } + }, to.hour, to.minute, false) } unitReportsViewModel.geofences.observe(viewLifecycleOwner) { geofences -> @@ -280,9 +379,10 @@ class UnitReportsFragment : Fragment() { EventInformation.Type.MAINTENANCE -> R.string.event_maintenance EventInformation.Type.TEXT_MESSAGE -> R.string.event_text_message EventInformation.Type.DRIVER_CHANGED -> R.string.event_driver_changed - EventInformation.Type.UNKNOWN -> R.string.event_unknown + EventInformation.Type.UNKNOWN -> R.string.event_unknown else -> R.string.event_unknown - }) + } + ) } event.geofence?.let { geofenceText.text = it.name diff --git a/androidApp/src/main/res/drawable/icon_down.xml b/androidApp/src/main/res/drawable/icon_down.xml new file mode 100644 index 0000000..884bee1 --- /dev/null +++ b/androidApp/src/main/res/drawable/icon_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/androidApp/src/main/res/drawable/icon_up.xml b/androidApp/src/main/res/drawable/icon_up.xml new file mode 100644 index 0000000..9b15755 --- /dev/null +++ b/androidApp/src/main/res/drawable/icon_up.xml @@ -0,0 +1,10 @@ + + + diff --git a/androidApp/src/main/res/layout/unit_details_reports.xml b/androidApp/src/main/res/layout/unit_details_reports.xml index 903c2c3..c29e4b0 100644 --- a/androidApp/src/main/res/layout/unit_details_reports.xml +++ b/androidApp/src/main/res/layout/unit_details_reports.xml @@ -1,194 +1,297 @@ - - - - - - - + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools"> - + + - + + + + + - + - + - + - + - + - + - + - + - + + + + + + + + + + app:behavior_hideable="false" + app:behavior_peekHeight="@dimen/bottom_sheet_peak" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" + style="@style/BottomSheetCardStyle"> + android:orientation="vertical"> - - - + android:layout_marginBottom="@dimen/margin" + android:orientation="horizontal"> - + + - - - - - - - + + + + + + + + + + + + + + android:orientation="horizontal"> - + - + + - + + android:gravity="center_vertical" + android:weightSum="2" + android:orientation="horizontal"> + + + + - + + + + + + + + + + + + + + android:orientation="horizontal" + android:layout_marginTop="@dimen/margin"> - + + + - + + + - + - \ No newline at end of file + \ No newline at end of file diff --git a/androidApp/src/main/res/values-es-rMX/strings.xml b/androidApp/src/main/res/values-es-rMX/strings.xml index d07b18d..f3fecb2 100644 --- a/androidApp/src/main/res/values-es-rMX/strings.xml +++ b/androidApp/src/main/res/values-es-rMX/strings.xml @@ -6,6 +6,8 @@ Cerrar Enviar Cargando + Deslizar hacia arriba + Deslizar hacia abajo Nombre de usuario @@ -113,6 +115,9 @@ Mes Últimos 30d Personalizado + + Inicio + Fin Evento Fecha y hora diff --git a/androidApp/src/main/res/values/dimen.xml b/androidApp/src/main/res/values/dimen.xml index 92d5242..8c8c5f5 100644 --- a/androidApp/src/main/res/values/dimen.xml +++ b/androidApp/src/main/res/values/dimen.xml @@ -35,6 +35,12 @@ 10dp 11sp + + 80dp + + + 12sp + 8dp 16dp diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index ce07188..93dd45f 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -20,6 +20,8 @@ Close Send Loading + Slide up + Slide down Username @@ -127,6 +129,9 @@ Month Last 30d Custom + + Start + End Event Datetime diff --git a/androidApp/src/main/res/values/styles.xml b/androidApp/src/main/res/values/styles.xml index dc40450..982d4dc 100644 --- a/androidApp/src/main/res/values/styles.xml +++ b/androidApp/src/main/res/values/styles.xml @@ -12,4 +12,17 @@ bold + + + + \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt b/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt new file mode 100644 index 0000000..35f1ac6 --- /dev/null +++ b/shared/src/androidMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt @@ -0,0 +1,23 @@ +package mx.trackermap.TrackerMap.utils + +import android.os.Build +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import java.text.SimpleDateFormat +import java.time.format.DateTimeFormatter +import java.util.* + +actual class DateUtils { + actual companion object { + actual fun formatDate(date: LocalDateTime): String { + val javaDate = date.toJavaLocalDateTime() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + return javaDate.format(formatter) + } else { + val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + return formatter.format(date) + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt index e120e97..bc0a48f 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt @@ -60,18 +60,17 @@ class ReportController( return } - val (currentDate, previousDate) = reportPeriod.getDates() + val (from, to) = reportPeriod.getStringDates() if (!xlsx) { reportFlow.value = Report.LoadingReport } - // GlobalScope.launch { - when (reportType) { - ReportType.POSITIONS -> fetchPositions(deviceId, previousDate, currentDate, xlsx) - ReportType.EVENTS -> fetchEvents(deviceId, previousDate, currentDate, eventTypes, xlsx) - ReportType.STOPS -> fetchStops(deviceId, previousDate, currentDate, xlsx) - } - // } + + when (reportType) { + ReportType.POSITIONS -> fetchPositions(deviceId, from, to, xlsx) + ReportType.EVENTS -> fetchEvents(deviceId, from, to, eventTypes, xlsx) + ReportType.STOPS -> fetchStops(deviceId, from, to, xlsx) + } } private suspend fun fetchPositions( diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt new file mode 100644 index 0000000..a85ae69 --- /dev/null +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt @@ -0,0 +1,9 @@ +package mx.trackermap.TrackerMap.utils + +import kotlinx.datetime.LocalDateTime + +expect class DateUtils { + companion object { + fun formatDate(date: LocalDateTime): String + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/Formatter.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/Formatter.kt index 77d0d14..bc7e622 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/Formatter.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/Formatter.kt @@ -23,9 +23,8 @@ class Formatter { companion object { fun formatDate(str: String): String { val timezone = TimeZone.currentSystemDefault() - val date = str.toInstant().toLocalDateTime(timezone).toString() - return date - .replace('T', ' ').trim('Z') + val datetime = str.toInstant().toLocalDateTime(timezone) + return DateUtils.formatDate(datetime) } fun formatSpeed(speed: Double, unit: SpeedUnit): String { diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt index 64df79d..6c86d27 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt @@ -29,81 +29,86 @@ class ReportDates { val dateTime = instant.toLocalDateTime(timezone) val date = dateTime.date - abstract fun getDates(): Pair + abstract fun getObjectDates(): Pair + + fun getStringDates(): Pair { + val (from, to) = getObjectDates() + return formatDateTime(from) to formatDateTime(to) + } fun formatDateTime(dateTime: LocalDateTime) = dateTime.toInstant(timezone).toString() class Today : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = date.atTime(0, 0) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } class Last24 : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = instant .minus(1, DateTimeUnit.DAY, timezone) .toLocalDateTime(timezone) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } class Yesterday : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val yesterday = instant .minus(1, DateTimeUnit.DAY, timezone) .toLocalDateTime(timezone).date val from = yesterday.atTime(0, 0) val to = yesterday.atTime(23, 59) - return formatDateTime(to) to formatDateTime(from) + return from to to } } class ThisWeek : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = instant .minus(date.dayOfWeek.isoDayNumber - 1, DateTimeUnit.DAY, timezone) .toLocalDateTime(timezone).date .atTime(0, 0) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } class Last7 : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = instant .minus(1, DateTimeUnit.WEEK, timezone) .toLocalDateTime(timezone).date .atTime(0, 0) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } class ThisMonth : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = instant .minus(date.dayOfMonth - 1, DateTimeUnit.DAY, timezone) .toLocalDateTime(timezone).date .atTime(0, 0) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } class Last30 : ReportPeriod() { - override fun getDates(): Pair { + override fun getObjectDates(): Pair { val from = instant .minus(1, DateTimeUnit.MONTH, timezone) .toLocalDateTime(timezone).date .atTime(0, 0) val to = dateTime - return formatDateTime(to) to formatDateTime(from) + return from to to } } @@ -111,13 +116,15 @@ class ReportDates { private val from: LocalDateTime? = null, private val to: LocalDateTime? = null ) : ReportPeriod() { - override fun getDates(): Pair { - return formatDateTime( + override fun getObjectDates(): Pair { + return Pair( + from ?: date.atTime(0, 0), to ?: dateTime - ) to formatDateTime( - from ?: date.atTime(0, 0) ) } + + fun withFrom (from: LocalDateTime): Custom = Custom (from, to) + fun withTo (to: LocalDateTime): Custom = Custom (from, to) } } } \ No newline at end of file -- cgit v1.2.3 From 10cb1152a2cb76609e6f65a0e7a2690013d3de16 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Tue, 12 Apr 2022 00:24:14 -0500 Subject: Initial version of custom reports in iOS --- .../iosApp/Details/Reports/UnitReportsView.swift | 130 ++++++++++----------- .../Details/Reports/UnitReportsViewModel.swift | 50 +++++++- iosApp/iosApp/en.lproj/Localizable.strings | 3 + iosApp/iosApp/es-419.lproj/Localizable.strings | 3 + .../mx/trackermap/TrackerMap/utils/ReportDates.kt | 11 ++ .../mx/trackermap/TrackerMap/utils/DateUtils.kt | 22 ++++ 6 files changed, 150 insertions(+), 69 deletions(-) create mode 100644 shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt (limited to 'shared/src') diff --git a/iosApp/iosApp/Details/Reports/UnitReportsView.swift b/iosApp/iosApp/Details/Reports/UnitReportsView.swift index e189835..4f4378f 100644 --- a/iosApp/iosApp/Details/Reports/UnitReportsView.swift +++ b/iosApp/iosApp/Details/Reports/UnitReportsView.swift @@ -31,6 +31,9 @@ struct UnitReportsView: View { self.unit = unit self._unitReportsViewModel = StateObject( wrappedValue: UnitReportsViewModel(deviceId: unit.device.id)) + // Source: https://stackoverflow.com/a/63607411 + UITableView.appearance().contentInset.top = -35 + UITableView.appearance().contentInset.bottom = -25 } private var eventsConfig: TablerListConfig { @@ -111,76 +114,71 @@ struct UnitReportsView: View { isReport: true) } - VStack { - HStack { - Text("report-period") - Spacer() - Picker(selection: $unitReportsViewModel.reportPeriod) { - Text("period-today").tag(ReportDates.ReportPeriod.today) - Text("period-last-24").tag(ReportDates.ReportPeriod.last24) - Text("period-yesterday").tag(ReportDates.ReportPeriod.yesterday) - Text("period-this-week").tag(ReportDates.ReportPeriod.thisWeek) - Text("period-last-7").tag(ReportDates.ReportPeriod.last7) - Text("period-this-month").tag(ReportDates.ReportPeriod.thisMonth) - Text("period-last-30").tag(ReportDates.ReportPeriod.last30) - } label: { - switch unitReportsViewModel.reportPeriod { - case .today: - Text("period-today") - case .last24: - Text("period-last-24") - case .yesterday: - Text("period-yesterday") - case .thisWeek: - Text("period-this-week") - case .last7: - Text("period-last-7") - case .thisMonth: - Text("period-this-month") - case .last30: - Text("period-last-30") - default: - Text("period-today") - } + // MARK: - Report type + Picker(selection: $unitReportsViewModel.reportType) { + Text("report-positions").tag(ReportController.ReportType.positions) + Text("report-events").tag(ReportController.ReportType.events) + Text("report-stops").tag(ReportController.ReportType.stops) + } label: { + EmptyView() + } + .pickerStyle(SegmentedPickerStyle()) + .padding() + + Form { + // MARK: - Report period + Section { + Picker(selection: $unitReportsViewModel.periodType, label: Text("report-period")) { + Text("period-today").tag(ReportDates.PeriodTypes.today) + Text("period-last-24").tag(ReportDates.PeriodTypes.last24) + Text("period-yesterday").tag(ReportDates.PeriodTypes.yesterday) + Text("period-this-week").tag(ReportDates.PeriodTypes.thisWeek) + Text("period-last-7").tag(ReportDates.PeriodTypes.last7) + Text("period-this-month").tag(ReportDates.PeriodTypes.thisMonth) + Text("period-last-30").tag(ReportDates.PeriodTypes.last30) + Text("period-custom").tag(ReportDates.PeriodTypes.custom) } - }.padding() - Picker(selection: $unitReportsViewModel.reportType) { - Text("report-positions").tag(ReportController.ReportType.positions) - Text("report-events").tag(ReportController.ReportType.events) - Text("report-stops").tag(ReportController.ReportType.stops) - } label: { - EmptyView() - }.pickerStyle(SegmentedPickerStyle()) - - HStack { - Group { - Button { - unitReportsViewModel.saveXlsxReport() - } label: { - Text("report-save") + + if unitReportsViewModel.periodType == .custom { + DatePicker(selection: $unitReportsViewModel.fromDate) { + Text("period-from") } - - Button { - unitReportsViewModel.shareXlsxReport() - } label: { - Text("report-share") - } - }.frame(maxWidth: .infinity) - }.padding() - .sheet(isPresented: $unitReportsViewModel.showShareDialog) { - ShareView(activityItems: $unitReportsViewModel.activityItems) - } - .fileExporter(isPresented: $unitReportsViewModel.showExportDialog, - documents: [unitReportsViewModel.saveDocument], - contentType: .xlsx) { result in - switch result { - case .success (let url): - print ("Saved to \(url)") - case .failure (let error): - print (error.localizedDescription) + + DatePicker(selection: $unitReportsViewModel.toDate) { + Text("period-to") } } - }.padding() + } + + // MARK: - Report actions + Section { + Button { + unitReportsViewModel.saveXlsxReport() + } label: { + Text("report-save") + }.frame(maxWidth: .infinity) + + Button { + unitReportsViewModel.shareXlsxReport() + } label: { + Text("report-share") + }.frame(maxWidth: .infinity) + } + } + .frame(maxHeight: 150) + .sheet(isPresented: $unitReportsViewModel.showShareDialog) { + ShareView(activityItems: $unitReportsViewModel.activityItems) + } + .fileExporter(isPresented: $unitReportsViewModel.showExportDialog, + documents: [unitReportsViewModel.saveDocument], + contentType: .xlsx) { result in + switch result { + case .success (let url): + print ("Saved to \(url)") + case .failure (let error): + print (error.localizedDescription) + } + } }.onAppear { unitReportsViewModel.fetchReport() } diff --git a/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift index e5b3e4b..450bfa7 100644 --- a/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift +++ b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift @@ -29,12 +29,38 @@ class UnitReportsViewModel: ObservableObject { } @Published var deviceId: Int32? = nil + + // MARK: - Report settings @Published var reportType: ReportController.ReportType = .positions { didSet { fetchReport() } } - @Published var reportPeriod: ReportDates.ReportPeriod = .today { + @Published var periodType: ReportDates.PeriodTypes = .today { + didSet { + switch periodType { + case .today: + reportPeriod = ReportDates.ReportPeriodToday() + case .last24: + reportPeriod = ReportDates.ReportPeriodLast24() + case .yesterday: + reportPeriod = ReportDates.ReportPeriodYesterday() + case .thisWeek: + reportPeriod = ReportDates.ReportPeriodThisWeek() + case .last7: + reportPeriod = ReportDates.ReportPeriodLast7() + case .thisMonth: + reportPeriod = ReportDates.ReportPeriodThisMonth() + case .last30: + reportPeriod = ReportDates.ReportPeriodLast30() + case .custom: + reportPeriod = ReportDates.ReportPeriodCustom(from: nil, to: nil) + default: + reportPeriod = ReportDates.ReportPeriodToday() + } + } + } + @Published var reportPeriod: ReportDates.ReportPeriod = ReportDates.ReportPeriodToday() { didSet { fetchReport() } @@ -67,7 +93,25 @@ class UnitReportsViewModel: ObservableObject { @Published var markers = [Marker]() @Published var selectedMarker: Marker? = nil - // Geofences + @Published var fromDate: Date = Date() { + didSet { + if let custom = reportPeriod as? ReportDates.ReportPeriodCustom { + reportPeriod = custom.withFrom( + from: DateUtils.companion.iosDateToKotlin(date: fromDate)) + } + } + } + + @Published var toDate: Date = Calendar.current.startOfDay(for: Date()) { + didSet { + if let custom = reportPeriod as? ReportDates.ReportPeriodCustom { + reportPeriod = custom.withTo( + to: DateUtils.companion.iosDateToKotlin(date: toDate)) + } + } + } + + // MARK: - Geofences @Published var geofences: [Int: Geofence] = [:] { didSet { flatGeofences = Array(geofences.values) @@ -75,7 +119,7 @@ class UnitReportsViewModel: ObservableObject { } @Published var flatGeofences: [Geofence] = [] - // Save and share + // MARK: - Save and share var xlsxAction: XlsxAction = .save var saveDocument = XlsxFile() @Published var showExportDialog: Bool = false diff --git a/iosApp/iosApp/en.lproj/Localizable.strings b/iosApp/iosApp/en.lproj/Localizable.strings index fee132b..ca1c3b8 100644 --- a/iosApp/iosApp/en.lproj/Localizable.strings +++ b/iosApp/iosApp/en.lproj/Localizable.strings @@ -67,6 +67,9 @@ "period-last-7" = "Last 7d"; "period-this-month" = "Month"; "period-last-30" = "Last 30d"; +"period-custom" = "Custom"; +"period-from" = "Start"; +"period-to" = "End"; "events-table-event" = "Event"; "events-table-datetime" = "Datetime"; diff --git a/iosApp/iosApp/es-419.lproj/Localizable.strings b/iosApp/iosApp/es-419.lproj/Localizable.strings index 0105e5d..146da98 100644 --- a/iosApp/iosApp/es-419.lproj/Localizable.strings +++ b/iosApp/iosApp/es-419.lproj/Localizable.strings @@ -67,6 +67,9 @@ "period-last-7" = "Últimos 7d"; "period-this-month" = "Mes"; "period-last-30" = "Últimos 30d"; +"period-custom" = "Personalizado"; +"period-from" = "Inicio"; +"period-to" = "Fin"; "events-table-event" = "Evento"; "events-table-datetime" = "Fecha y hora"; diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt index 6c86d27..5298df3 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/utils/ReportDates.kt @@ -22,6 +22,17 @@ import kotlinx.datetime.* @DelicateCoroutinesApi class ReportDates { + enum class PeriodTypes { + TODAY, + LAST_24, + YESTERDAY, + THIS_WEEK, + LAST_7, + THIS_MONTH, + LAST_30, + CUSTOM + } + sealed class ReportPeriod { val timezone = TimeZone.currentSystemDefault() val clock = Clock.System diff --git a/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt new file mode 100644 index 0000000..7286ea5 --- /dev/null +++ b/shared/src/iosMain/kotlin/mx/trackermap/TrackerMap/utils/DateUtils.kt @@ -0,0 +1,22 @@ +package mx.trackermap.TrackerMap.utils + +import kotlinx.datetime.* +import platform.Foundation.NSDate +import platform.Foundation.NSDateFormatter + +actual class DateUtils { + actual companion object { + fun iosDateToKotlin(date: NSDate): LocalDateTime { + val timezone = TimeZone.currentSystemDefault() + return date.toKotlinInstant().toLocalDateTime(timezone) + } + + actual fun formatDate(date: LocalDateTime): String { + val timezone = TimeZone.currentSystemDefault() + val iosDate = date.toInstant(timezone).toNSDate() + val formatter = NSDateFormatter() + formatter.setDateFormat("yyyy-MM-dd HH:mm") + return formatter.stringFromDate(iosDate) + } + } +} \ No newline at end of file -- cgit v1.2.3