Parcourir la source

Merge pull request #10 from MrOzOn/add_module_measure

Add module measure
MrOzOn il y a 5 ans
Parent
commit
2c610bc235
66 fichiers modifiés avec 2294 ajouts et 21 suppressions
  1. 1 0
      app/build.gradle
  2. 8 0
      app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt
  3. 17 0
      app/src/main/java/com/mrozon/healthdiary/navigation/EditMeasureNavigatorImpl.kt
  4. 24 0
      app/src/main/java/com/mrozon/healthdiary/navigation/ListMeasureNavigatorImpl.kt
  5. 5 0
      app/src/main/java/com/mrozon/healthdiary/navigation/ListPersonNavigatorImpl.kt
  6. 55 0
      app/src/main/res/navigation/nav_graph.xml
  7. 1 1
      app/src/main/res/values/styles.xml
  8. 2 1
      core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt
  9. 35 0
      core_api/src/main/java/com/mrozon/core_api/db/dao/MeasureDao.kt
  10. 6 0
      core_api/src/main/java/com/mrozon/core_api/db/dao/MeasureTypeDao.kt
  11. 3 0
      core_api/src/main/java/com/mrozon/core_api/db/dao/UserDao.kt
  12. 34 0
      core_api/src/main/java/com/mrozon/core_api/db/model/MeasureDb.kt
  13. 15 0
      core_api/src/main/java/com/mrozon/core_api/entity/Measure.kt
  14. 39 0
      core_api/src/main/java/com/mrozon/core_api/mapper/MeasureToMeasureDbMapper.kt
  15. 8 0
      core_api/src/main/java/com/mrozon/core_api/navigation/EditMeasureNavigator.kt
  16. 9 0
      core_api/src/main/java/com/mrozon/core_api/navigation/ListMeasureNavigator.kt
  17. 2 0
      core_api/src/main/java/com/mrozon/core_api/navigation/ListPersonNavigator.kt
  18. 2 0
      core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt
  19. 13 13
      core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt
  20. 11 0
      core_api/src/main/java/com/mrozon/core_api/network/model/MeasureRequest.kt
  21. 34 0
      core_api/src/main/java/com/mrozon/core_api/network/model/MeasureResponse.kt
  22. 58 2
      core_impl/schemas/com.mrozon.core_impl.db.HealthDiaryDb/1.json
  23. 2 1
      core_impl/src/main/java/com/mrozon/core_impl/db/HealthDiaryDb.kt
  24. 1 0
      feature_measure/.gitignore
  25. 84 0
      feature_measure/build.gradle
  26. 0 0
      feature_measure/consumer-rules.pro
  27. 21 0
      feature_measure/proguard-rules.pro
  28. 24 0
      feature_measure/src/androidTest/java/com/mrozon/feature_measure/ExampleInstrumentedTest.kt
  29. 5 0
      feature_measure/src/main/AndroidManifest.xml
  30. 31 0
      feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRemoteDataSource.kt
  31. 28 0
      feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRepository.kt
  32. 192 0
      feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRepositoryImpl.kt
  33. 31 0
      feature_measure/src/main/java/com/mrozon/feature_measure/di/DaggerViewModelFactory.kt
  34. 50 0
      feature_measure/src/main/java/com/mrozon/feature_measure/di/TabMeasureFragmentComponent.kt
  35. 37 0
      feature_measure/src/main/java/com/mrozon/feature_measure/di/TabMeasureFragmentModule.kt
  36. 43 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/BindingUtils.kt
  37. 208 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/EditMeasureFragment.kt
  38. 194 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/EditMeasureFragmentViewModel.kt
  39. 62 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureAdapter.kt
  40. 169 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureFragment.kt
  41. 61 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureFragmentViewModel.kt
  42. 18 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureAdapter.kt
  43. 139 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureFragment.kt
  44. 36 0
      feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureFragmentViewModel.kt
  45. 5 0
      feature_measure/src/main/res/color/tab_color_selector.xml
  46. 5 0
      feature_measure/src/main/res/drawable/ic_baseline_delete_outline_24.xml
  47. 5 0
      feature_measure/src/main/res/drawable/ic_check_white_24dp.xml
  48. 14 0
      feature_measure/src/main/res/drawable/ic_day.xml
  49. 16 0
      feature_measure/src/main/res/drawable/ic_edit.xml
  50. 6 0
      feature_measure/src/main/res/drawable/ic_evening.xml
  51. 9 0
      feature_measure/src/main/res/drawable/ic_morning.xml
  52. 7 0
      feature_measure/src/main/res/drawable/ic_night.xml
  53. 5 0
      feature_measure/src/main/res/drawable/ic_refresh_24.xml
  54. 120 0
      feature_measure/src/main/res/layout/fragment_edit_measure.xml
  55. 55 0
      feature_measure/src/main/res/layout/fragment_list_measure.xml
  56. 54 0
      feature_measure/src/main/res/layout/fragment_tab_measure.xml
  57. 101 0
      feature_measure/src/main/res/layout/item_measure.xml
  58. 16 0
      feature_measure/src/main/res/menu/add_measure_menu.xml
  59. 11 0
      feature_measure/src/main/res/menu/list_measure_menu.xml
  60. 4 0
      feature_measure/src/main/res/values/colors.xml
  61. 15 0
      feature_measure/src/main/res/values/strings.xml
  62. 17 0
      feature_measure/src/test/java/com/mrozon/feature_measure/ExampleUnitTest.kt
  63. 1 2
      feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt
  64. 3 1
      scripts/deps_versions.gradle
  65. 1 0
      settings.gradle
  66. 6 0
      utils/src/main/java/com/mrozon/utils/extension/FragmentExt.kt

+ 1 - 0
app/build.gradle

@@ -87,6 +87,7 @@ dependencies {
     implementation project(':feature_auth')
     implementation project(':feature_person')
     implementation project(':feature_measure_type')
+    implementation project(':feature_measure')
 }
 
 def prepareProperties() {

+ 8 - 0
app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt

@@ -28,4 +28,12 @@ interface NavigationModule {
     @Reusable
     @Binds
     fun editPersonNavigator(navigator: EditPersonNavigatorImpl): EditPersonNavigator
+
+    @Reusable
+    @Binds
+    fun editMeasureNavigator(navigator: EditMeasureNavigatorImpl): EditMeasureNavigator
+
+    @Reusable
+    @Binds
+    fun listMeasureNavigator(navigator: ListMeasureNavigatorImpl): ListMeasureNavigator
 }

+ 17 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/EditMeasureNavigatorImpl.kt

@@ -0,0 +1,17 @@
+package com.mrozon.healthdiary.navigation
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavController
+import com.mrozon.core_api.navigation.EditMeasureNavigator
+import com.mrozon.core_api.navigation.EditPersonNavigator
+import com.mrozon.healthdiary.R
+import javax.inject.Inject
+
+class EditMeasureNavigatorImpl @Inject constructor()
+    : EditMeasureNavigator {
+
+    override fun navigateToListMeasure(navController: NavController, id: Long, measureTypeId: Long) {
+        val bundle = bundleOf("id" to id, "measureTypeId" to measureTypeId)
+        navController.navigate(R.id.action_editMeasureFragment_to_listMeasureFragment, bundle)
+    }
+}

+ 24 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/ListMeasureNavigatorImpl.kt

@@ -0,0 +1,24 @@
+package com.mrozon.healthdiary.navigation
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
+import com.mrozon.core_api.navigation.ListMeasureNavigator
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.healthdiary.R
+import javax.inject.Inject
+
+class ListMeasureNavigatorImpl @Inject constructor()
+    : ListMeasureNavigator {
+
+    override fun navigateToEditMeasure(
+        navController: NavController,
+        title: String,
+        id: Long,
+        personId: Long,
+        measureTypeId: Long
+    ) {
+        val bundle = bundleOf("title" to title, "id" to id, "personId" to personId, "measureTypeId" to measureTypeId)
+        navController.navigate(R.id.action_tabMeasureFragment_to_editMeasureFragment, bundle)
+    }
+}

+ 5 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/ListPersonNavigatorImpl.kt

@@ -14,4 +14,9 @@ class ListPersonNavigatorImpl @Inject constructor()
         val bundle = bundleOf("title" to title, "id" to id)
         navController.navigate(R.id.action_listPersonFragment_to_editPersonFragment, bundle)
     }
+
+    override fun navigateToMeasureForPerson(navController: NavController, id: Long) {
+        val bundle = bundleOf( "id" to id)
+        navController.navigate(R.id.action_listPersonFragment_to_tabMeasureFragment, bundle)
+    }
 }

+ 55 - 0
app/src/main/res/navigation/nav_graph.xml

@@ -90,6 +90,13 @@
             app:exitAnim="@anim/slide_out_left"
             app:popEnterAnim="@anim/slide_in_left"
             app:popExitAnim="@anim/slide_out_right"/>
+        <action
+            android:id="@+id/action_listPersonFragment_to_tabMeasureFragment"
+            app:destination="@id/tabMeasureFragment"
+            app:enterAnim="@anim/slide_in_right"
+            app:exitAnim="@anim/slide_out_left"
+            app:popEnterAnim="@anim/slide_in_left"
+            app:popExitAnim="@anim/slide_out_right"/>
     </fragment>
 
     <fragment
@@ -128,4 +135,52 @@
         app:popEnterAnim="@anim/slide_in_left"
         app:popExitAnim="@anim/slide_out_right"/>
 
+    <fragment
+        android:id="@+id/tabMeasureFragment"
+        android:name="com.mrozon.feature_measure.presentation.TabMeasureFragment"
+        tools:layout="@layout/fragment_tab_measure"
+        android:label="TabMeasureFragment" >
+        <action
+            android:id="@+id/action_tabMeasureFragment_to_editMeasureFragment"
+            app:destination="@id/editMeasureFragment"
+            app:enterAnim="@anim/slide_in_right"
+            app:exitAnim="@anim/slide_out_left"
+            app:popEnterAnim="@anim/slide_in_left"
+            app:popExitAnim="@anim/slide_out_right"/>
+    </fragment>
+
+    <fragment
+        android:id="@+id/listMeasureFragment"
+        android:name="com.mrozon.feature_measure.presentation.ListMeasureFragment"
+        tools:layout="@layout/fragment_list_measure"
+        android:label="ListMeasureFragment" />
+
+    <fragment
+        android:id="@+id/editMeasureFragment"
+        android:name="com.mrozon.feature_measure.presentation.EditMeasureFragment"
+        tools:layout="@layout/fragment_edit_measure"
+        android:label="{title}" >
+        <argument
+            android:name="title"
+            app:argType="string" />
+        <argument
+            android:name="id"
+            app:argType="long" />
+        <argument
+            android:name="personId"
+            app:argType="long" />
+        <argument
+            android:name="measureTypeId"
+            app:argType="long" />
+        <action
+            android:id="@+id/action_editMeasureFragment_to_listMeasureFragment"
+            app:destination="@id/tabMeasureFragment"
+            app:enterAnim="@anim/slide_in_right"
+            app:exitAnim="@anim/slide_out_left"
+            app:launchSingleTop="true"
+            app:popEnterAnim="@anim/slide_in_left"
+            app:popExitAnim="@anim/slide_out_right"
+            app:popUpToInclusive="true" />
+    </fragment>
+
 </navigation>

+ 1 - 1
app/src/main/res/values/styles.xml

@@ -1,7 +1,7 @@
 <resources>
 
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+    <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
         <!-- Customize your theme here. -->
         <item name="colorPrimary">@color/colorPrimary</item>
         <item name="colorPrimaryDark">@color/colorPrimaryDark</item>

+ 2 - 1
core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt

@@ -1,9 +1,10 @@
 package com.mrozon.core_api.db
 
 import androidx.room.Dao
+import com.mrozon.core_api.db.dao.MeasureDao
 import com.mrozon.core_api.db.dao.MeasureTypeDao
 import com.mrozon.core_api.db.dao.PersonDao
 import com.mrozon.core_api.db.dao.UserDao
 
 @Dao
-interface HealthDiaryDao: UserDao, PersonDao, MeasureTypeDao
+interface HealthDiaryDao: UserDao, PersonDao, MeasureTypeDao, MeasureDao

+ 35 - 0
core_api/src/main/java/com/mrozon/core_api/db/dao/MeasureDao.kt

@@ -0,0 +1,35 @@
+package com.mrozon.core_api.db.dao
+
+import androidx.room.*
+import com.mrozon.core_api.db.model.MeasureDb
+import com.mrozon.core_api.db.model.MeasureTypeDb
+import com.mrozon.core_api.db.model.PersonDb
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface MeasureDao {
+
+    @Query("SELECT * FROM measure_table WHERE measure_person=:personId AND measure_mtype=:measureTypeId ORDER BY measure_value_added DESC")
+    fun getMeasures(personId: Long, measureTypeId: Long): List<MeasureDb>
+
+    @Query("SELECT * FROM measure_table WHERE measure_id=:id LIMIT 1")
+    fun getMeasure(id: Long): MeasureDb
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertAllMeasure(measures: List<MeasureDb>)
+
+    @Query("DELETE FROM measure_table")
+    suspend fun deleteAllMeasure()
+
+    @Transaction
+    suspend fun reloadMeasure(measures: List<MeasureDb>) {
+        deleteAllMeasure()
+        insertAllMeasure(measures)
+    }
+
+    @Query("DELETE FROM measure_table WHERE measure_id=:id")
+    suspend fun deleteMeasure(id: Long)
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertMeasure(measureDb: MeasureDb)
+}

+ 6 - 0
core_api/src/main/java/com/mrozon/core_api/db/dao/MeasureTypeDao.kt

@@ -23,4 +23,10 @@ interface MeasureTypeDao {
     @Query("SELECT * FROM measure_type_table")
     fun getMeasureTypes(): Flow<List<MeasureTypeDb>>
 
+    @Query("SELECT * FROM measure_type_table")
+    fun getListMeasureTypes(): List<MeasureTypeDb>
+
+    @Query("SELECT * FROM measure_type_table WHERE measure_type_id=:id LIMIT 1")
+    fun getMeasureType(id: Long): MeasureTypeDb
+
 }

+ 3 - 0
core_api/src/main/java/com/mrozon/core_api/db/dao/UserDao.kt

@@ -14,6 +14,9 @@ interface UserDao {
     @Query("SELECT * FROM user_table LIMIT 1")
     fun getLiveUser(): LiveData<UserDb>
 
+    @Query("SELECT * FROM user_table LIMIT 1")
+    suspend fun getSuspendUser(): UserDb
+
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     suspend fun insertUser(userDb: UserDb)
 

+ 34 - 0
core_api/src/main/java/com/mrozon/core_api/db/model/MeasureDb.kt

@@ -0,0 +1,34 @@
+package com.mrozon.core_api.db.model
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.TypeConverters
+import com.mrozon.core_api.db.BlobTransmogrifier
+import java.util.*
+
+@Entity(tableName = "measure_table")
+@TypeConverters(BlobTransmogrifier::class)
+data class MeasureDb (
+    @PrimaryKey(autoGenerate = true)
+    @ColumnInfo(name = "measure_id")
+    var id: Long = 0L,
+
+    @ColumnInfo(name = "measure_value1")
+    val value1: String = "",
+
+    @ColumnInfo(name = "measure_value2")
+    val value2: String = "",
+
+    @ColumnInfo(name = "measure_value_added")
+    val added: Date = Date(),
+
+    @ColumnInfo(name = "measure_comment")
+    val comment: String = "",
+
+    @ColumnInfo(name = "measure_person")
+    val personID: Long = 0L,
+
+    @ColumnInfo(name = "measure_mtype")
+    val measureTypeId: Long = 0L
+)

+ 15 - 0
core_api/src/main/java/com/mrozon/core_api/entity/Measure.kt

@@ -0,0 +1,15 @@
+package com.mrozon.core_api.entity
+
+import java.util.*
+
+data class Measure (
+    val id: Long = 0L,
+    val value1: String,
+    val value2: String,
+    val valueAdded: Date,
+    val comment: String,
+    val personId: Long,
+    val measureTypeId: Long
+)
+
+typealias MeasureHistory = Triple<Person, MeasureType, List<Measure>>

+ 39 - 0
core_api/src/main/java/com/mrozon/core_api/mapper/MeasureToMeasureDbMapper.kt

@@ -0,0 +1,39 @@
+package com.mrozon.core_api.mapper
+
+import com.mrozon.core_api.db.model.MeasureDb
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.utils.base.BaseMapper
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MeasureToMeasureDbMapper @Inject constructor(): BaseMapper<Measure, MeasureDb>()  {
+
+    override fun map(entity: Measure?): MeasureDb? {
+        entity?.let {
+            return MeasureDb(id = it.id,
+                value1 = it.value1,
+                value2 = it.value2,
+                added = it.valueAdded,
+                comment = it.comment,
+                personID = it.personId,
+                measureTypeId = it.measureTypeId
+            )
+        }
+        return null
+    }
+
+    override fun reverseMap(model: MeasureDb?): Measure? {
+        model?.let {
+            return Measure(id = it.id,
+                value1 = it.value1,
+                value2 = it.value2,
+                valueAdded = it.added,
+                comment = it.comment,
+                personId = it.personID,
+                measureTypeId = it.measureTypeId
+            )
+        }
+        return null
+    }
+}

+ 8 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/EditMeasureNavigator.kt

@@ -0,0 +1,8 @@
+package com.mrozon.core_api.navigation
+
+import androidx.navigation.NavController
+
+interface EditMeasureNavigator {
+
+    fun navigateToListMeasure(navController: NavController, id: Long, measureTypeId: Long)
+}

+ 9 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/ListMeasureNavigator.kt

@@ -0,0 +1,9 @@
+package com.mrozon.core_api.navigation
+
+import androidx.navigation.NavController
+
+interface ListMeasureNavigator {
+
+    fun navigateToEditMeasure(navController: NavController, title: String, id: Long, personId: Long, measureTypeId: Long )
+
+}

+ 2 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/ListPersonNavigator.kt

@@ -5,4 +5,6 @@ import androidx.navigation.NavController
 interface ListPersonNavigator {
 
     fun navigateToEditPerson(navController: NavController, title: String, id: Long)
+
+    fun navigateToMeasureForPerson(navController: NavController, id: Long)
 }

+ 2 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt

@@ -6,4 +6,6 @@ interface NavigatorProvider {
     fun provideRegistrationNavigator(): RegistrationNavigator
     fun provideListPersonNavigator(): ListPersonNavigator
     fun provideEditPersonNavigator(): EditPersonNavigator
+    fun provideListMeasureNavigator(): ListMeasureNavigator
+    fun provideEditMeasureNavigator(): EditMeasureNavigator
 }

+ 13 - 13
core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt

@@ -35,17 +35,17 @@ interface HealthDiaryService {
     @GET("indicatortypes/")
     suspend fun getMeasureTypes(): Response<List<MeasureTypeResponse>>
 
-//    @GET("lego/themes/")
-//    suspend fun getThemes(@Query("page") page: Int? = null,
-//                          @Query("page_size") pageSize: Int? = null,
-//                          @Query("ordering") order: String? = null): Response<ResultsResponse<LegoTheme>>
-//
-//    @GET("lego/sets/")
-//    suspend fun getSets(@Query("page") page: Int? = null,
-//                        @Query("page_size") pageSize: Int? = null,
-//                        @Query("theme_id") themeId: Int? = null,
-//                        @Query("ordering") order: String? = null): Response<ResultsResponse<LegoSet>>
-//
-//    @GET("lego/sets/{id}/")
-//    suspend fun getSet(@Path("id") id: String): Response<LegoSet>
+    @GET("indicators/")
+    suspend fun getMeasure(@Query("type") type: Long,
+                           @Query("patient") patient: Long): Response<List<MeasureResponse>>
+
+    @DELETE("indicators/{id}/")
+    suspend fun deleteMeasure(@Path("id") id: String): Response<Unit>
+
+    @POST("indicators/")
+    suspend fun addMeasure(@Body body: MeasureRequest): Response<MeasureResponse>
+
+    @PUT("indicators/{id}/")
+    suspend fun editMeasure(@Path("id") id: String,
+                           @Body body: MeasureRequest): Response<MeasureResponse>
 }

+ 11 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/MeasureRequest.kt

@@ -0,0 +1,11 @@
+package com.mrozon.core_api.network.model
+
+data class MeasureRequest(
+    val value1: String,
+    val value2: String,
+    val value_added: String,
+    val comments: String,
+    val type: Long,
+    val patient: Long,
+    val observing: Long
+)

+ 34 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/MeasureResponse.kt

@@ -0,0 +1,34 @@
+package com.mrozon.core_api.network.model
+
+import com.google.gson.annotations.SerializedName
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.utils.extension.toSimpleDate
+
+data class MeasureResponse(
+	val comments: String? = null,
+	val value2: String? = null,
+	val value1: String,
+	val patient: Int,
+	val observing: Int,
+	val id: Int,
+	@SerializedName("created_date")
+	val createdDate: String,
+	@SerializedName("value_added")
+	val valueAdded: String,
+	val type: Int,
+	@SerializedName("last_modified")
+	val lastModified: String
+)
+
+fun MeasureResponse.toMeasure(): Measure {
+	return Measure(
+		id = id.toLong(),
+		value1 = value1,
+		value2 = value2?:"",
+		valueAdded = valueAdded.toSimpleDate(format = "yyyy-MM-dd'T'HH:mm:ss"), //2020-07-23T17:39:02+03:00
+		comment = comments?:"",
+		personId = patient.toLong(),
+		measureTypeId = type.toLong()
+	)
+}
+

+ 58 - 2
core_impl/schemas/com.mrozon.core_impl.db.HealthDiaryDb/1.json

@@ -2,7 +2,7 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "1fbfe00709ec7129d957473024f00645",
+    "identityHash": "2079a1d49cc2a92c04780e48139987df",
     "entities": [
       {
         "tableName": "user_table",
@@ -135,12 +135,68 @@
         },
         "indices": [],
         "foreignKeys": []
+      },
+      {
+        "tableName": "measure_table",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`measure_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `measure_value1` TEXT NOT NULL, `measure_value2` TEXT NOT NULL, `measure_value_added` INTEGER NOT NULL, `measure_comment` TEXT NOT NULL, `measure_person` INTEGER NOT NULL, `measure_mtype` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "measure_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value1",
+            "columnName": "measure_value1",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "value2",
+            "columnName": "measure_value2",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "added",
+            "columnName": "measure_value_added",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "comment",
+            "columnName": "measure_comment",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "personID",
+            "columnName": "measure_person",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "measureTypeId",
+            "columnName": "measure_mtype",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "measure_id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
       }
     ],
     "views": [],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1fbfe00709ec7129d957473024f00645')"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2079a1d49cc2a92c04780e48139987df')"
     ]
   }
 }

+ 2 - 1
core_impl/src/main/java/com/mrozon/core_impl/db/HealthDiaryDb.kt

@@ -3,9 +3,10 @@ package com.mrozon.core_impl.db
 import androidx.room.Database
 import androidx.room.RoomDatabase
 import com.mrozon.core_api.db.HealthDiaryDatabaseContract
+import com.mrozon.core_api.db.model.MeasureDb
 import com.mrozon.core_api.db.model.MeasureTypeDb
 import com.mrozon.core_api.db.model.PersonDb
 import com.mrozon.core_api.db.model.UserDb
 
-@Database(entities = [UserDb::class, PersonDb::class, MeasureTypeDb::class], version = 1)
+@Database(entities = [UserDb::class, PersonDb::class, MeasureTypeDb::class, MeasureDb::class], version = 1)
 abstract class HealthDiaryDb : RoomDatabase(), HealthDiaryDatabaseContract

+ 1 - 0
feature_measure/.gitignore

@@ -0,0 +1 @@
+/build

+ 84 - 0
feature_measure/build.gradle

@@ -0,0 +1,84 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+
+apply from: "$project.rootDir/jacoco.gradle"
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "29.0.3"
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        debug {
+            testCoverageEnabled true
+        }
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    dataBinding {
+        enabled = true
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+}
+
+apply from: "$project.rootDir/scripts/deps_versions.gradle"
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+    implementation kotlinStdlib
+
+    api project(':core_api')
+    implementation project(':core')
+    implementation project(':utils')
+    //DAGGER
+    implementation dagger
+    kapt daggerCompiler
+    implementation constraintlayout
+    //Timber
+    implementation timber
+    implementation navigationFragment
+    implementation retrofit
+    //AndroidX
+    implementation legacySupport
+    implementation appCompat
+    implementation androidxCore
+    implementation cardview
+    implementation recyclerview
+    implementation material
+    implementation viewpager2
+    implementation coil
+    implementation coilBase
+    implementation coilSvg
+    //Unit test
+    testImplementation junit
+    testImplementation mockitoCore
+    testImplementation kotlinxCoroutinesTest
+    testImplementation robolectric
+    testImplementation androidXCoreTest
+    //Instrumental Test
+    androidTestImplementation junitInstrumental
+    androidTestImplementation androidXRulesTest
+    androidTestImplementation androidXRunnerTest
+    androidTestImplementation espressoCore
+    androidTestImplementation androidXFragmentTest
+    kaptAndroidTest daggerCompiler
+}

+ 0 - 0
feature_measure/consumer-rules.pro


+ 21 - 0
feature_measure/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
feature_measure/src/androidTest/java/com/mrozon/feature_measure/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.mrozon.feature_measure
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.mrozon.feature_measure.test", appContext.packageName)
+    }
+}

+ 5 - 0
feature_measure/src/main/AndroidManifest.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.mrozon.feature_measure">
+
+</manifest>

+ 31 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRemoteDataSource.kt

@@ -0,0 +1,31 @@
+package com.mrozon.feature_measure.data
+
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.core_api.network.model.MeasureRequest
+import com.mrozon.core_api.network.model.PersonRequest
+import com.mrozon.utils.base.BaseDataSource
+import javax.inject.Inject
+
+class MeasureRemoteDataSource @Inject constructor(private val service: HealthDiaryService): BaseDataSource() {
+
+    suspend fun getMeasure(personId: Long, measureTypeId: Long)
+            = getResult {
+        service.getMeasure(measureTypeId, personId)
+    }
+
+    suspend fun deleteMeasure(id: Long)
+            = getResult {
+        service.deleteMeasure(id.toString())
+    }
+
+    suspend fun addMeasure(request: MeasureRequest)
+            = getResult {
+        service.addMeasure(request)
+    }
+
+    suspend fun editMeasure(id: Long, request: MeasureRequest)
+            = getResult {
+        service.editMeasure(id.toString(), request)
+    }
+
+}

+ 28 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRepository.kt

@@ -0,0 +1,28 @@
+package com.mrozon.feature_measure.data
+
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureHistory
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.flow.Flow
+
+interface MeasureRepository {
+
+    fun loadProfilePersonAndMeasureTypes(id: Long): Flow<Result<Pair<Person,List<MeasureType>>>>
+
+    fun loadMeasure(personId: Long, measureTypeId: Long): Flow<Result<MeasureHistory>>
+
+    fun loadMeasureOnlyNetwork(personId: Long, measureTypeId: Long): Flow<Result<List<Measure>>>
+
+    fun loadSelectedPersonAndMeasureTypes(personId: Long, measureTypeId: Long): Flow<Result<Pair<Person,MeasureType>>>
+
+    fun loadSelectedMeasure(id: Long): Flow<Result<Measure>>
+
+    fun deleteMeasure(id: Long): Flow<Result<Unit>>
+
+    fun addMeasure(measure: Measure): Flow<Result<Unit>>
+
+    fun editMeasure(measure: Measure): Flow<Result<Unit>>
+
+}

+ 192 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/data/MeasureRepositoryImpl.kt

@@ -0,0 +1,192 @@
+package com.mrozon.feature_measure.data
+
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureHistory
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.mapper.MeasureToMeasureDbMapper
+import com.mrozon.core_api.mapper.MeasureTypeToMeasureTypeDbMapper
+import com.mrozon.core_api.mapper.PersonToPersonDbMapper
+import com.mrozon.core_api.network.model.MeasureRequest
+import com.mrozon.core_api.network.model.toMeasure
+import com.mrozon.core_api.network.model.toPerson
+import com.mrozon.utils.extension.toDateString
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class MeasureRepositoryImpl @Inject constructor(
+    private val dao: HealthDiaryDao,
+    private val dataSource: MeasureRemoteDataSource,
+    private val mapperPerson: PersonToPersonDbMapper,
+    private val mapperMeasureType: MeasureTypeToMeasureTypeDbMapper,
+    private val mapperMeasure: MeasureToMeasureDbMapper
+): MeasureRepository {
+
+    override fun loadProfilePersonAndMeasureTypes(id: Long): Flow<Result<Pair<Person, List<MeasureType>>>> {
+        return flow {
+            emit(Result.loading())
+            try {
+                val person = mapperPerson.reverseMap(dao.getPerson(id))
+                val measureTypes = mapperMeasureType.reverseMap(dao.getListMeasureTypes())
+                emit(Result.success(Pair(person!!, measureTypes)))
+            }
+            catch (e: Exception){
+                emit(Result.error(e.message!!))
+            }
+        }
+    }
+
+    override fun loadMeasure(personId: Long, measureTypeId: Long): Flow<Result<MeasureHistory>> {
+        return  flow {
+            emit(Result.loading())
+            try {
+                val person = mapperPerson.reverseMap(dao.getPerson(personId))!!
+                val measureType = mapperMeasureType.reverseMap(dao.getMeasureType(measureTypeId))!!
+                val measures = mapperMeasure.reverseMap(dao.getMeasures(personId, measureTypeId))
+
+                emit(Result.success(Triple(person, measureType, measures.sortedByDescending { it.valueAdded })))
+
+                val networkResult = dataSource.getMeasure(personId, measureTypeId)
+                if (networkResult.status == Result.Status.SUCCESS) {
+                    val data = networkResult.data!!
+                    val measureList = data.map { measureResponse ->
+                        measureResponse.toMeasure()
+                    }
+                    val measuresDb = mapperMeasure.map(measureList)
+                    dao.reloadMeasure(measuresDb)
+                    emit(Result.success(Triple(person, measureType, measureList)))
+                    } else if (networkResult.status == Result.Status.ERROR) {
+                        emit(Result.error(networkResult.message!!))
+                    }
+                }
+            catch (e: Exception){
+                emit(Result.error(e.message!!))
+            }
+        }
+    }
+
+    override fun loadMeasureOnlyNetwork(personId: Long, measureTypeId: Long): Flow<Result<List<Measure>>> {
+        return  flow {
+            emit(Result.loading())
+            try {
+                val networkResult = dataSource.getMeasure(personId, measureTypeId)
+                if (networkResult.status == Result.Status.SUCCESS) {
+                    val data = networkResult.data!!
+                    val measureList = data.map { measureResponse ->
+                        measureResponse.toMeasure()
+                    }
+                    val measuresDb = mapperMeasure.map(measureList)
+                    dao.reloadMeasure(measuresDb)
+                    emit(Result.success(measureList))
+                } else if (networkResult.status == Result.Status.ERROR) {
+                    emit(Result.error(networkResult.message!!))
+                }
+            }
+            catch (e: Exception){
+                emit(Result.error(e.message!!))
+            }
+        }
+    }
+
+    override fun loadSelectedPersonAndMeasureTypes(
+        personId: Long,
+        measureTypeId: Long
+    ): Flow<Result<Pair<Person, MeasureType>>> {
+        return  flow {
+            emit(Result.loading())
+            try {
+                val personDb = dao.getPerson(personId)
+                val measureTypeDb = dao.getMeasureType(measureTypeId)
+                emit(Result.success(Pair(mapperPerson.reverseMap(personDb)!!, mapperMeasureType.reverseMap(measureTypeDb)!!)))
+            }
+            catch (e: Exception){
+                emit(Result.error(e.message!!))
+            }
+        }
+    }
+
+    override fun loadSelectedMeasure(id: Long): Flow<Result<Measure>> {
+        return  flow {
+            emit(Result.loading())
+            try {
+                val measureDb = dao.getMeasure(id)
+                emit(Result.success(mapperMeasure.reverseMap(measureDb)!!))
+            }
+            catch (e: Exception){
+                Timber.e(e)
+                emit(Result.error(e.message!!))
+            }
+        }
+    }
+
+    override fun deleteMeasure(id: Long): Flow<Result<Unit>> {
+        return flow {
+            emit(Result.loading())
+            val response = dataSource.deleteMeasure(id)
+            if (response.status == Result.Status.SUCCESS) {
+                dao.deleteMeasure(id)
+                emit(Result.success())
+            } else if (response.status == Result.Status.ERROR) {
+                emit(Result.error(response.message!!))
+            }
+        }
+    }
+
+    override fun addMeasure(measure: Measure): Flow<Result<Unit>> {
+        return flow {
+            emit(Result.loading())
+            val observing = dao.getSuspendUser().id
+            val request = MeasureRequest (
+                value1 = measure.value1,
+                value2 = measure.value2,
+                value_added = measure.valueAdded.toDateString(format = "yyyy-MM-dd'T'HH:mm:ss"),
+                comments = measure.comment,
+                type = measure.measureTypeId,
+                patient = measure.personId,
+                observing = observing
+            )
+            val response = dataSource.addMeasure(request)
+            if (response.status == Result.Status.SUCCESS) {
+                val measureDb = mapperMeasure.map(response.data?.toMeasure())
+                dao.insertMeasure(measureDb!!)
+                emit(Result.success())
+            } else if (response.status == Result.Status.ERROR) {
+                emit(Result.error(response.message!!))
+            }
+        }
+    }
+
+    override fun editMeasure(measure: Measure): Flow<Result<Unit>> {
+        return flow {
+            emit(Result.loading())
+            val observing = dao.getSuspendUser().id
+            val request = MeasureRequest (
+                value1 = measure.value1,
+                value2 = measure.value2,
+                value_added = measure.valueAdded.toDateString(format = "yyyy-MM-dd'T'HH:mm:ss"),
+                comments = measure.comment,
+                type = measure.measureTypeId,
+                patient = measure.personId,
+                observing = observing
+            )
+            val response = dataSource.editMeasure(measure.id, request)
+            if (response.status == Result.Status.SUCCESS) {
+                val measureDb = mapperMeasure.map(response.data?.toMeasure())
+                dao.insertMeasure(measureDb!!)
+                emit(Result.success())
+            } else if (response.status == Result.Status.ERROR) {
+                emit(Result.error(response.message!!))
+            }
+        }
+    }
+
+
+
+
+}

+ 31 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/di/DaggerViewModelFactory.kt

@@ -0,0 +1,31 @@
+package com.mrozon.feature_measure.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+import javax.inject.Provider
+
+class DaggerViewModelFactory @Inject constructor(
+    private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>
+) : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        var creator: Provider<out ViewModel>? = creators[modelClass]
+        if (creator == null) {
+            for ((key, value) in creators) {
+                if (modelClass.isAssignableFrom(key)) {
+                    creator = value
+                    break
+                }
+            }
+        }
+        if (creator == null) {
+            throw IllegalArgumentException("Unknown model class: $modelClass")
+        }
+        try {
+            @Suppress("UNCHECKED_CAST")
+            return creator.get() as T
+        } catch (e: Exception) {
+            throw RuntimeException(e)
+        }
+    }
+}

+ 50 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/di/TabMeasureFragmentComponent.kt

@@ -0,0 +1,50 @@
+package com.mrozon.feature_measure.di
+
+import com.mrozon.core_api.providers.AppWithFacade
+import com.mrozon.core_api.providers.ProvidersFacade
+import com.mrozon.feature_measure.presentation.EditMeasureFragment
+import com.mrozon.feature_measure.presentation.ListMeasureFragment
+import com.mrozon.feature_measure.presentation.TabMeasureFragment
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = [TabMeasureFragmentModule::class],
+    dependencies = [ProvidersFacade::class]
+)
+interface TabMeasureFragmentComponent {
+
+    companion object {
+        fun create(providersFacade: ProvidersFacade): TabMeasureFragmentComponent {
+            return DaggerTabMeasureFragmentComponent.builder()
+                .providersFacade(providersFacade)
+                .build()
+        }
+        fun injectFragment(fragment: TabMeasureFragment): TabMeasureFragmentComponent  {
+            val component = create((fragment.activity?.application
+                    as AppWithFacade).getFacade())
+            component.inject(fragment)
+            return component
+        }
+
+        fun injectFragment(fragment: ListMeasureFragment): TabMeasureFragmentComponent  {
+            val component = create((fragment.activity?.application
+                    as AppWithFacade).getFacade())
+            component.inject(fragment)
+            return component
+        }
+
+        fun injectFragment(fragment: EditMeasureFragment): TabMeasureFragmentComponent  {
+            val component = create((fragment.activity?.application
+                    as AppWithFacade).getFacade())
+            component.inject(fragment)
+            return component
+        }
+    }
+
+    fun inject(fragment: TabMeasureFragment)
+    fun inject(fragment: ListMeasureFragment)
+    fun inject(fragment: EditMeasureFragment)
+
+}

+ 37 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/di/TabMeasureFragmentModule.kt

@@ -0,0 +1,37 @@
+package com.mrozon.feature_measure.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.mrozon.core_api.viewmodel.ViewModelKey
+import com.mrozon.feature_measure.data.MeasureRepository
+import com.mrozon.feature_measure.data.MeasureRepositoryImpl
+import com.mrozon.feature_measure.presentation.EditMeasureFragmentViewModel
+import com.mrozon.feature_measure.presentation.ListMeasureFragmentViewModel
+import com.mrozon.feature_measure.presentation.TabMeasureFragmentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+interface TabMeasureFragmentModule {
+    @Binds
+    @IntoMap
+    @ViewModelKey(TabMeasureFragmentViewModel::class)
+    fun bindTabMeasureFragmentViewModel(viewmodel: TabMeasureFragmentViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(ListMeasureFragmentViewModel::class)
+    fun bindListMeasureFragmentViewModel(viewmodel: ListMeasureFragmentViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(EditMeasureFragmentViewModel::class)
+    fun bindEditMeasureFragmentViewModel(viewmodel: EditMeasureFragmentViewModel): ViewModel
+
+    @Binds
+    fun viewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
+
+    @Binds
+    fun providePersonRepository(repository: MeasureRepositoryImpl): MeasureRepository
+}

+ 43 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/BindingUtils.kt

@@ -0,0 +1,43 @@
+package com.mrozon.feature_measure.presentation
+
+import android.annotation.SuppressLint
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.feature_measure.R
+import com.mrozon.utils.extension.toDateString
+import java.util.*
+
+@BindingAdapter("time_of_day")
+fun ImageView.setTimeOfDay(measure: Measure) {
+    val date = measure.valueAdded
+    val cal = Calendar.getInstance()
+    cal.time = date
+    setImageResource(when (cal.get(Calendar.HOUR_OF_DAY)) {
+        in 0..5 -> R.drawable.ic_night
+        in 6..10 -> R.drawable.ic_morning
+        in 11..18 -> R.drawable.ic_day
+        in 19..22 -> R.drawable.ic_evening
+        in 23..24 -> R.drawable.ic_night
+        else -> R.drawable.ic_day
+    })
+}
+
+@BindingAdapter("added_date")
+fun TextView.setDateAdded(measure: Measure) {
+    text = measure.valueAdded.toDateString("EEE, d MMM HH:mm")
+}
+
+@SuppressLint("SetTextI18n")
+@BindingAdapter(value = ["measure","measure_type"])
+fun TextView.setMeasureValue(measure: Measure, measureType: MeasureType) {
+    text = if (measure.value2.isEmpty()) {
+        "${measure.value1} ${measureType.mark}"
+    }
+    else {
+        "${measure.value1}/${measure.value2} ${measureType.mark}"
+    }
+
+}

+ 208 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/EditMeasureFragment.kt

@@ -0,0 +1,208 @@
+package com.mrozon.feature_measure.presentation
+
+import android.content.Context
+import android.os.Bundle
+import android.view.*
+import androidx.core.app.ActivityCompat
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.MaterialTimePicker.INPUT_MODE_CLOCK
+import com.google.android.material.timepicker.TimeFormat
+import com.mrozon.core_api.navigation.EditMeasureNavigator
+import com.mrozon.core_api.navigation.ListMeasureNavigator
+import com.mrozon.feature_measure.R
+import com.mrozon.feature_measure.databinding.FragmentEditMeasureBinding
+import com.mrozon.feature_measure.di.TabMeasureFragmentComponent
+import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.*
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import timber.log.Timber
+import javax.inject.Inject
+
+class EditMeasureFragment: BaseFragment<FragmentEditMeasureBinding>() {
+
+    override fun getLayoutId(): Int = R.layout.fragment_edit_measure
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    @Inject
+    lateinit var navigator: EditMeasureNavigator
+
+    private val viewModel by viewModels<EditMeasureFragmentViewModel> { viewModelFactory }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        TabMeasureFragmentComponent.injectFragment(this)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        setHasOptionsMenu(true)
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    @ExperimentalCoroutinesApi
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        arguments?.let {
+            val id = requireArguments().getLong("id", 0)
+            val personId = requireArguments().getLong("personId", 0)
+            val measureTypeId = requireArguments().getLong("measureTypeId", 0)
+
+            viewModel.initialLoadData(id, personId, measureTypeId)
+        }
+
+        binding?.buttonChooseDate?.setOnClickListener {
+            try {
+                val builder = MaterialDatePicker.Builder.datePicker()
+                    .apply {
+                        setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+                        setSelection(viewModel.currentDatetime.value?.time)
+                    }
+                val picker = builder.build()
+                picker.addOnPositiveButtonClickListener {
+                    viewModel.changeDate(it)
+                }
+                picker.show(childFragmentManager, picker.toString())
+            } catch (e: IllegalArgumentException) {
+                Timber.e(e)
+                showError(e.message!!)
+            }
+        }
+
+        binding?.buttonChooseTime?.setOnClickListener {
+            try {
+                val picker = MaterialTimePicker.Builder()
+                    .setTimeFormat(TimeFormat.CLOCK_24H)
+                    .setInputMode(INPUT_MODE_CLOCK)
+                    .setHour(viewModel.currentHour)
+                    .setMinute(viewModel.currentMinute)
+                    .build()
+                picker.addOnPositiveButtonClickListener {
+                    viewModel.changeTime(picker.hour, picker.minute)
+                }
+                picker.show(childFragmentManager, picker.toString())
+            } catch (e: IllegalArgumentException) {
+                Timber.e(e)
+                showError(e.message!!)
+            }
+        }
+
+        binding?.tilComment?.editText?.offer(viewModel.commentChannel)
+        binding?.tilMeasureValue?.editText?.offer(viewModel.measureValueChannel)
+    }
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.add_measure_menu, menu)
+        val currentId = requireArguments().getLong("id", 0)
+        val deleteMenuItem = menu.findItem(R.id.deleteMeasure)
+        deleteMenuItem.isVisible = currentId>0
+        val saveMenuItem = menu.findItem(R.id.saveMeasure)
+        saveMenuItem.isVisible = binding?.tilMeasureValue?.error == null && binding?.tilMeasureValue?.editText?.text?.isNotEmpty()!!
+        return super.onCreateOptionsMenu(menu, inflater)
+    }
+
+    @ExperimentalCoroutinesApi
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        hideKeyboard()
+        if (!isActiveNetwork()){
+            showError(getString(R.string.network_inactive))
+            return false
+        }
+        when(item.itemId){
+            R.id.saveMeasure -> {
+                val currentId = requireArguments().getLong("id", 0)
+                viewModel.saveMeasure(currentId)
+            }
+            R.id.deleteMeasure -> {
+                viewModel.deleteMeasure()
+            }
+        }
+        return false
+    }
+
+
+    @FlowPreview
+    @ExperimentalCoroutinesApi
+    override fun subscribeUi() {
+        viewModel.measureType.observe(viewLifecycleOwner, Observer { measureType ->
+            measureType?.let {
+                binding?.tilMeasureValue?.hint = getString(
+                    R.string.hint_value_added,
+                    it.name,
+                    it.mark
+                )
+            }
+        })
+        viewModel.currentDatetime.observe(viewLifecycleOwner, Observer { datetime ->
+            datetime?.let {
+                binding?.tvMeasureAddedDate?.text = it.toDateString("EEE, d MMM YY HH:mm:ss")
+            }
+        })
+        viewModel.measure.observe(viewLifecycleOwner, Observer { event ->
+            event.peekContent().let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        val measure = result.data
+                        measure?.let {
+                            binding?.tilComment?.editText?.setText(measure.comment)
+                            var measureValue = measure.value1
+                            if(measure.value2.isNotEmpty()) {
+                                measureValue +="/"+measure.value2
+                            }
+                            binding?.tilMeasureValue?.editText?.setText(measureValue)
+                        }
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
+                }
+            }
+        })
+        viewModel.correctMeasureValue.observe(viewLifecycleOwner, Observer {correct ->
+            correct?.let {
+                if(correct) {
+                    binding?.tilMeasureValue?.error = null
+                } else {
+                    binding?.tilMeasureValue?.error = getString(R.string.error_value, viewModel.measureType.value?.hint)
+                }
+                ActivityCompat.invalidateOptionsMenu(activity)
+            }
+        })
+        viewModel.goToListMeasure.observe(viewLifecycleOwner, Observer { event ->
+            event.getContentIfNotHandled()?.let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        navigator.navigateToListMeasure(findNavController(), viewModel.person.value?.id?:0, viewModel.measureType.value?.id?:0)
+
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
+                }
+            }
+        })
+    }
+}

+ 194 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/EditMeasureFragmentViewModel.kt

@@ -0,0 +1,194 @@
+package com.mrozon.feature_measure.presentation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.providers.CoroutineContextProvider
+import com.mrozon.feature_measure.data.MeasureRepository
+import com.mrozon.utils.Event
+import com.mrozon.utils.base.BaseViewModel
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import java.time.Year
+import java.util.*
+import java.util.regex.Pattern
+import javax.inject.Inject
+
+class EditMeasureFragmentViewModel @Inject constructor(
+    private val repository: MeasureRepository,
+    private val coroutineContextProvider: CoroutineContextProvider
+): BaseViewModel() {
+
+    private var _measureType = MutableLiveData<MeasureType>()
+    val measureType: LiveData<MeasureType>
+        get() = _measureType
+
+    private var _person = MutableLiveData<Person>()
+    val person: LiveData<Person>
+        get() = _person
+
+    private var _measure = MutableLiveData<Event<Result<Measure>>>()
+    val measure: LiveData<Event<Result<Measure>>>
+        get() = _measure
+
+    private var _currentDatetime = MutableLiveData<Date>()
+    val currentDatetime: LiveData<Date>
+        get() = _currentDatetime
+
+    private var _goToListMeasure = MutableLiveData<Event<Result<Unit>>>()
+    val goToListMeasure: LiveData<Event<Result<Unit>>>
+        get() = _goToListMeasure
+
+    val currentHour: Int
+        get() {
+            val currentCalendar = Calendar.getInstance()
+            currentCalendar.time = _currentDatetime.value!!
+            return currentCalendar.get(Calendar.HOUR_OF_DAY)
+        }
+
+    val currentMinute: Int
+        get() {
+            val currentCalendar = Calendar.getInstance()
+            currentCalendar.time = _currentDatetime.value!!
+            return currentCalendar.get(Calendar.MINUTE)
+        }
+
+    @ExperimentalCoroutinesApi
+    val commentChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val measureValueChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    val correctMeasureValue = object: MutableLiveData<Boolean>() {
+
+        override fun onActive() {
+            value?.let { return }
+            viewModelScope.launch {
+                var job: Deferred<Unit>? = null
+                measureValueChannel.asFlow()
+                    .collect {
+                        job?.cancel()
+                        job = async(Dispatchers.Main) {
+                            val pattern = Pattern.compile(measureType.value?.regexp?:"")
+                            val matcher = pattern.matcher(it)
+                            value = matcher.matches()
+                        }
+                    }
+            }
+        }
+    }
+
+    fun initialLoadData(id: Long, personId: Long, measureTypeId: Long) {
+        if(_measureType.value==null){
+//            _measure.value = Event(Result.loading())
+            viewModelScope.launch(coroutineContextProvider.IO) {
+                repository.loadSelectedPersonAndMeasureTypes(personId, measureTypeId).collect {
+                    withContext(coroutineContextProvider.Main) {
+                        if(it.status== Result.Status.SUCCESS) {
+                            _measureType.value = it.data?.second!!
+                            _person.value = it.data?.first!!
+                        }
+                    }
+                }
+                if (id>0){
+                    repository.loadSelectedMeasure(id).collect {
+                        withContext(coroutineContextProvider.Main) {
+                            _measure.value = Event(it)
+                            if (it.status == Result.Status.SUCCESS) {
+                                _currentDatetime.value = it.data?.valueAdded
+                            }
+                        }
+                    }
+                } else {
+                    withContext(coroutineContextProvider.Main) {
+                        _currentDatetime.value = Date()
+                    }
+                }
+            }
+//            _measure.value = Event(Result.success())
+        }
+    }
+
+    fun changeDate(value: Long?) {
+        value?.let {
+            val currentCalendar = Calendar.getInstance()
+            currentCalendar.time = _currentDatetime.value!!
+            val newDate = Calendar.getInstance()
+            newDate.time = Date(value)
+            currentCalendar.set(Calendar.YEAR,newDate.get(Calendar.YEAR))
+            currentCalendar.set(Calendar.MONTH,newDate.get(Calendar.MONTH))
+            currentCalendar.set(Calendar.DAY_OF_YEAR,newDate.get(Calendar.DAY_OF_YEAR))
+            _currentDatetime.value = currentCalendar.time
+        }
+    }
+
+    fun changeTime(hour: Int, minute: Int) {
+        val currentCalendar = Calendar.getInstance()
+        currentCalendar.time = _currentDatetime.value!!
+        currentCalendar.set(Calendar.HOUR_OF_DAY,hour)
+        currentCalendar.set(Calendar.MINUTE,minute)
+        currentCalendar.set(Calendar.SECOND,0)
+        _currentDatetime.value = currentCalendar.time
+    }
+
+    fun deleteMeasure() {
+        _measure.value?.let {
+            viewModelScope.launch(coroutineContextProvider.IO) {
+                val id = _measure.value?.peekContent()?.data?.id?:0
+                repository.deleteMeasure(id).collect {
+                    withContext(Dispatchers.Main) {
+                        _goToListMeasure.value = Event(it)
+                    }
+                }
+            }
+        }
+    }
+
+    @ExperimentalCoroutinesApi
+    fun saveMeasure(id: Long) {
+        val newMeasureValue = measureValueChannel.value
+        var value1 = newMeasureValue
+        var value2 = ""
+        if(newMeasureValue.contains('/')) {
+            val splits = newMeasureValue.split('/')
+            if(splits.size>1){
+                value1 = splits[0]
+                value2 = splits[1]
+            }
+        }
+
+        viewModelScope.launch(coroutineContextProvider.IO) {
+            val personId = _person.value?.id?:0
+            val measureTypeId = _measureType.value?.id?:0
+            val measure = Measure(id = id, personId = personId, measureTypeId = measureTypeId,
+                comment = commentChannel.value, valueAdded = _currentDatetime.value!!,
+                value1 = value1, value2 = value2
+            )
+            if(id>0) {
+                repository.editMeasure(measure).collect {
+                    withContext(Dispatchers.Main) {
+                        _goToListMeasure.value = Event(it)
+                    }
+                }
+            } else {
+                repository.addMeasure(measure).collect {
+                    withContext(Dispatchers.Main) {
+                        _goToListMeasure.value = Event(it)
+                    }
+                }
+            }
+        }
+
+    }
+
+
+}

+ 62 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureAdapter.kt

@@ -0,0 +1,62 @@
+package com.mrozon.feature_measure.presentation
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.feature_measure.databinding.ItemMeasureBinding
+
+class ListMeasureAdapter(private val measureType: MeasureType, private val clickListener: ListMeasureClickListener): ListAdapter<Measure, ListMeasureAdapter.ViewHolder>(ListMeasureDiffCallback()) {
+
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): ViewHolder = ViewHolder.from(parent)
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val item = getItem(position)
+        holder.bind(item, measureType, clickListener)
+    }
+
+    class ViewHolder private constructor(val binding: ItemMeasureBinding ): RecyclerView.ViewHolder(binding.root){
+        fun bind(
+            item: Measure,
+            measureType: MeasureType,
+            clickListener: ListMeasureClickListener) {
+            binding.measure = item
+            binding.measureType = measureType
+            binding.layoutMeasure.setOnClickListener {
+                clickListener.onClick(item)
+            }
+            binding.executePendingBindings()
+        }
+
+        companion object {
+            fun from(parent: ViewGroup): ViewHolder {
+                val layoutInflater = LayoutInflater.from(parent.context)
+                val binding =
+                    ItemMeasureBinding.inflate(layoutInflater, parent, false)
+                return ViewHolder(binding)
+            }
+        }
+    }
+
+    class ListMeasureDiffCallback : DiffUtil.ItemCallback<Measure>() {
+
+        override fun areItemsTheSame(oldItem: Measure, newItem: Measure): Boolean {
+            return oldItem.id == newItem.id
+        }
+
+        override fun areContentsTheSame(oldItem: Measure, newItem: Measure): Boolean {
+            return oldItem == newItem
+        }
+    }
+
+    interface ListMeasureClickListener {
+        fun onClick(measure: Measure)
+    }
+}

+ 169 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureFragment.kt

@@ -0,0 +1,169 @@
+package com.mrozon.feature_measure.presentation
+
+import android.content.Context
+import android.os.Bundle
+import android.view.*
+import android.widget.Toast
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.navigation.ListMeasureNavigator
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.feature_measure.R
+import com.mrozon.feature_measure.databinding.FragmentListMeasureBinding
+import com.mrozon.feature_measure.di.TabMeasureFragmentComponent
+import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.hideKeyboard
+import com.mrozon.utils.extension.setTitleActionBar
+import com.mrozon.utils.extension.visible
+import com.mrozon.utils.network.Result
+import timber.log.Timber
+import javax.inject.Inject
+
+class ListMeasureFragment : BaseFragment<FragmentListMeasureBinding>() {
+
+    companion object {
+        private const val ARG_PERSON_ID = "person_id"
+        private const val ARG_MEASURE_TYPE_ID = "measure_type_id"
+
+        fun getInstance(personId: Long, measureTypeId: Long): ListMeasureFragment {
+            val fragment = ListMeasureFragment()
+            fragment.arguments = Bundle().apply {
+                putLong(ARG_PERSON_ID, personId)
+                putLong(ARG_MEASURE_TYPE_ID, measureTypeId)
+            }
+            return fragment
+        }
+    }
+
+    @Inject
+    lateinit var navigator: ListMeasureNavigator
+
+    private var adapter: ListMeasureAdapter? = null
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        setHasOptionsMenu(true)
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        val manager = LinearLayoutManager(context)
+        binding?.rvMeasure?.apply {
+            layoutManager = manager
+            addItemDecoration(DividerItemDecoration(requireContext(),DividerItemDecoration.VERTICAL))
+        }
+        binding?.fabAddMeasure?.setOnClickListener { _ ->
+            arguments?.let {
+                val personId = requireArguments().getLong(ARG_PERSON_ID, 0)
+                val measureTypeId = requireArguments().getLong(ARG_MEASURE_TYPE_ID, 0)
+                navigator.navigateToEditMeasure(findNavController(),getString(R.string.add_measure),0, personId, measureTypeId)
+            }
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.list_measure_menu, menu)
+        return super.onCreateOptionsMenu(menu, inflater)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        hideKeyboard()
+        when(item.itemId){
+            R.id.refreshMeasuresNetwork -> {
+                viewModel.refreshMeasuresNetwork()
+            }
+        }
+        return false
+    }
+
+    override fun onResume() {
+        super.onResume()
+        arguments?.let {
+            val personId = requireArguments().getLong(ARG_PERSON_ID, -1)
+            val measureTypeId = requireArguments().getLong(ARG_MEASURE_TYPE_ID, -1)
+            Timber.d("onResume - $measureTypeId")
+            viewModel.initialLoadData(personId, measureTypeId)
+        }
+    }
+
+    override fun getLayoutId(): Int = R.layout.fragment_list_measure
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private val viewModel by viewModels<ListMeasureFragmentViewModel> { viewModelFactory }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        TabMeasureFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
+    }
+
+
+
+    override fun subscribeUi() {
+        viewModel.initialData.observe(viewLifecycleOwner, Observer { event ->
+            event.peekContent().let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        val person = result.data?.first
+                        setTitleActionBar(person?.name?:"")
+                        val measureType = result.data?.second
+                        val measures = result.data?.third
+                        Timber.d("measures contains ${measures?.size} items")
+                        if(adapter==null)
+                            adapter = ListMeasureAdapter(measureType!!, object:
+                                ListMeasureAdapter.ListMeasureClickListener {
+                                override fun onClick(measure: Measure) {
+                                    arguments?.let {
+                                        val personId = requireArguments().getLong(ARG_PERSON_ID, 0)
+                                        val measureTypeId = requireArguments().getLong(ARG_MEASURE_TYPE_ID, 0)
+                                        navigator.navigateToEditMeasure(findNavController(),getString(R.string.edit_measure),measure.id, personId, measureTypeId)
+                                    }
+                                }
+                            })
+                        binding?.rvMeasure?.adapter = adapter
+                        adapter?.submitList(measures)
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
+                }
+            }
+        })
+
+        viewModel.measures.observe(viewLifecycleOwner, Observer { event ->
+            event.peekContent().let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        adapter?.submitList(result.data)
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
+                }
+            }
+        })
+    }
+
+}

+ 61 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/ListMeasureFragmentViewModel.kt

@@ -0,0 +1,61 @@
+package com.mrozon.feature_measure.presentation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.entity.Measure
+import com.mrozon.core_api.entity.MeasureHistory
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.providers.CoroutineContextProvider
+import com.mrozon.feature_measure.data.MeasureRepository
+import com.mrozon.utils.Event
+import com.mrozon.utils.base.BaseViewModel
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import javax.inject.Inject
+
+class ListMeasureFragmentViewModel @Inject constructor(
+    private val repository: MeasureRepository,
+    private val coroutineContextProvider: CoroutineContextProvider
+): BaseViewModel() {
+
+    private var _initialData = MutableLiveData<Event<Result<MeasureHistory>>>()
+    val initialData: LiveData<Event<Result<MeasureHistory>>>
+        get() = _initialData
+
+    private var _measures = MutableLiveData<Event<Result<List<Measure>>>>()
+    val measures: LiveData<Event<Result<List<Measure>>>>
+        get() = _measures
+
+    fun initialLoadData(personId: Long, measureTypeId: Long) {
+//        if(_initialData.value == null) {
+            viewModelScope.launch(coroutineContextProvider.IO) {
+                repository.loadMeasure(personId, measureTypeId).collect {
+                    withContext(coroutineContextProvider.Main) {
+                        _initialData.value = Event(it)
+                    }
+                }
+            }
+//        }
+    }
+
+    fun refreshMeasuresNetwork() {
+        if(_initialData.value == null)
+            return
+        val personId = _initialData.value?.peekContent()?.data?.first?.id?:-1
+        val measureTypeId = _initialData.value?.peekContent()?.data?.second?.id?:-1
+        viewModelScope.launch(coroutineContextProvider.IO) {
+            repository.loadMeasureOnlyNetwork(personId, measureTypeId).collect {
+                withContext(coroutineContextProvider.Main) {
+                    _measures.value = Event(it)
+                }
+            }
+        }
+    }
+
+
+}

+ 18 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureAdapter.kt

@@ -0,0 +1,18 @@
+package com.mrozon.feature_measure.presentation
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import com.mrozon.core_api.entity.MeasureType
+
+class TabMeasureAdapter(
+    fragment: Fragment,
+    private val personId: Long,
+    private val measureTypes: List<MeasureType>) : FragmentStateAdapter(fragment) {
+
+    override fun getItemCount(): Int = measureTypes.size
+
+    override fun createFragment(position: Int): Fragment {
+        val id = measureTypes[position].id
+        return ListMeasureFragment.getInstance(personId, id)
+    }
+}

+ 139 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureFragment.kt

@@ -0,0 +1,139 @@
+package com.mrozon.feature_measure.presentation
+
+import android.content.Context
+import android.graphics.*
+import android.os.Bundle
+import android.view.View
+import android.widget.TableLayout
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.fragment.app.FragmentStatePagerAdapter
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.observe
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import coil.ImageLoader
+import coil.decode.SvgDecoder
+import coil.load
+import coil.request.Disposable
+import coil.request.ImageRequest
+import coil.size.Scale
+import com.google.android.material.tabs.TabItem
+import com.google.android.material.tabs.TabLayout
+import com.google.android.material.tabs.TabLayout.MODE_SCROLLABLE
+import com.google.android.material.tabs.TabLayoutMediator
+import com.mrozon.core_api.entity.Gender
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.feature_measure.R
+import com.mrozon.feature_measure.databinding.FragmentTabMeasureBinding
+import com.mrozon.feature_measure.di.TabMeasureFragmentComponent
+import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.setTitleActionBar
+import com.mrozon.utils.extension.visible
+import com.mrozon.utils.network.Result
+import timber.log.Timber
+import java.util.*
+import javax.inject.Inject
+
+class TabMeasureFragment: BaseFragment<FragmentTabMeasureBinding>() {
+
+    override fun getLayoutId(): Int = R.layout.fragment_tab_measure
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private val viewModel by viewModels<TabMeasureFragmentViewModel> { viewModelFactory }
+
+    private val disposables: List<Disposable> = mutableListOf()
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        TabMeasureFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val id = arguments?.getLong("id", 0)?:0
+        if(id>0){
+            viewModel.loadProfilePersonAndMeasureTypes(id)
+        }
+//        binding?.measureTypesTabs?.isClickable = true
+//        binding?.measureTypesTabs?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener{
+//            override fun onTabSelected(tab: TabLayout.Tab?) {
+//                Timber.d("onTabSelected ${tab?.position}")
+////                binding?.viewpager?.setCurrentItem(tab?.position!!)
+//            }
+//
+//            override fun onTabUnselected(tab: TabLayout.Tab?) {
+//                Timber.d("onTabUnselected")
+//            }
+//
+//            override fun onTabReselected(tab: TabLayout.Tab?) {
+//                Timber.d("onTabReselected")
+//            }
+//
+//        })
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        disposables.forEach {
+            it.dispose()
+        }
+    }
+
+    override fun subscribeUi() {
+        viewModel.selectedPersonAndMeasureTypes.observe(viewLifecycleOwner, Observer { event ->
+            event.peekContent().let { result ->
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        if(arguments?.containsKey("id") == true)
+                            arguments?.remove("id")
+                        binding?.progressBar?.visible(false)
+                        val person = result.data?.first
+                        val measureTypes = result.data?.second
+                        setTitleActionBar(person?.name?:"")
+                        binding?.viewpager?.adapter = TabMeasureAdapter(this,person?.id?:-1,
+                            measureTypes?: listOf())
+
+                        measureTypes?.let {
+                            TabLayoutMediator(binding?.measureTypesTabs!!, binding?.viewpager!!) { tab, position ->
+//                                tab.text = measureTypes[position].name
+                                val request = ImageRequest.Builder(requireContext())
+                                    .decoder(SvgDecoder(requireContext()))
+                                    .data(HealthDiaryService.ENDPOINT + measureTypes[position].url)
+                                    .size(48,48)
+                                    .scale(Scale.FILL)
+                                    .target { drawable ->
+                                        tab.icon = drawable
+                                    }
+                                    .build()
+                                disposables.plus(ImageLoader(requireContext()).enqueue(request))
+                            }.attach()
+                            val measureTypeId = requireArguments().getLong("measureTypeId", 0)
+                            if(measureTypeId>0){
+                                for ((index, value) in measureTypes.withIndex()) {
+                                    if (value.id==measureTypeId) {
+                                        binding?.viewpager?.currentItem = index
+                                        break
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!)
+                    }
+                }
+            }
+        })
+    }
+}

+ 36 - 0
feature_measure/src/main/java/com/mrozon/feature_measure/presentation/TabMeasureFragmentViewModel.kt

@@ -0,0 +1,36 @@
+package com.mrozon.feature_measure.presentation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.entity.MeasureType
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.providers.CoroutineContextProvider
+import com.mrozon.feature_measure.data.MeasureRepository
+import com.mrozon.utils.Event
+import com.mrozon.utils.base.BaseViewModel
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class TabMeasureFragmentViewModel @Inject constructor(
+    private val repository: MeasureRepository,
+    private val coroutineContextProvider: CoroutineContextProvider
+): BaseViewModel() {
+
+    private var _selectedPersonAndMeasureTypes = MutableLiveData<Event<Result<Pair<Person, List<MeasureType>>>>>()
+    val selectedPersonAndMeasureTypes: LiveData<Event<Result<Pair<Person, List<MeasureType>>>>>
+        get() = _selectedPersonAndMeasureTypes
+
+    fun loadProfilePersonAndMeasureTypes(id: Long) {
+            viewModelScope.launch(coroutineContextProvider.IO) {
+                repository.loadProfilePersonAndMeasureTypes(id).collect {
+                    withContext(coroutineContextProvider.Main) {
+                        _selectedPersonAndMeasureTypes.value = Event(it)
+                    }
+                }
+            }
+    }
+}

+ 5 - 0
feature_measure/src/main/res/color/tab_color_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/white" android:state_selected="true"/>
+    <item android:color="#AAFFFFFF"/>
+</selector>

+ 5 - 0
feature_measure/src/main/res/drawable/ic_baseline_delete_outline_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
+</vector>

+ 5 - 0
feature_measure/src/main/res/drawable/ic_check_white_24dp.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>

+ 14 - 0
feature_measure/src/main/res/drawable/ic_day.xml

@@ -0,0 +1,14 @@
+<vector android:height="55dp" android:viewportHeight="300"
+    android:viewportWidth="300" android:width="55dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha="0.75" android:fillColor="#5CCAFF" android:pathData="M300,150C300,232.843 232.843,300 150,300C87.232,300 33.468,261.447 11.098,206.728C3.943,189.228 0,170.075 0,150C0,67.157 67.157,0 150,0C232.843,0 300,67.157 300,150Z"/>
+    <path android:fillColor="#D6FFFB" android:fillType="evenOdd" android:pathData="M92.682,288.483C88.062,280.647 85.411,271.511 85.411,261.756C85.411,244.682 93.532,229.504 106.123,219.875C104.266,214.489 103.258,208.708 103.258,202.691C103.258,173.591 126.848,150 155.949,150C160.978,150 165.842,150.704 170.448,152.02C179.914,137.984 195.961,128.754 214.164,128.754C237.036,128.754 256.504,143.326 263.79,163.693C267.642,162.797 271.655,162.323 275.779,162.323C284.26,162.323 292.272,164.326 299.369,167.886C290.528,242.297 227.215,300 150.425,300C129.962,300 110.456,295.902 92.682,288.483Z"/>
+    <path android:fillColor="#FFE021" android:pathData="M118.98,78.399a31.657,31.657 0,1 0,63.314 0a31.657,31.657 0,1 0,-63.314 0z"/>
+    <path android:fillColor="#FFE021" android:pathData="M196.53,73.513L211.402,73.513A4.887,4.887 0,0 1,216.289 78.399L216.289,78.399A4.887,4.887 0,0 1,211.402 83.286L196.53,83.286A4.887,4.887 0,0 1,191.643 78.399L191.643,78.399A4.887,4.887 0,0 1,196.53 73.513z"/>
+    <path android:fillColor="#FFE021" android:pathData="M88.598,73.513L103.47,73.513A4.887,4.887 0,0 1,108.357 78.399L108.357,78.399A4.887,4.887 0,0 1,103.47 83.286L88.598,83.286A4.887,4.887 0,0 1,83.711 78.399L83.711,78.399A4.887,4.887 0,0 1,88.598 73.513z"/>
+    <path android:fillColor="#FFE021" android:pathData="M185.861,107.988L196.378,118.505A4.887,4.887 0,0 1,196.378 125.416L196.378,125.416A4.887,4.887 0,0 1,189.467 125.416L178.951,114.899A4.887,4.887 90,0 1,178.951 107.988L178.951,107.988A4.887,4.887 90,0 1,185.861 107.988z"/>
+    <path android:fillColor="#FFE021" android:pathData="M109.799,31.501L120.316,42.017A4.887,4.887 45.169,0 1,120.316 48.928L120.316,48.928A4.887,4.887 45.169,0 1,113.405 48.928L102.889,38.412A4.887,4.887 45.169,0 1,102.889 31.501L102.889,31.501A4.887,4.887 45.169,0 1,109.799 31.501z"/>
+    <path android:fillColor="#FFE021" android:pathData="M145.751,31.232L145.751,16.36A4.887,4.887 0,0 1,150.638 11.473L150.638,11.473A4.887,4.887 0,0 1,155.524 16.36L155.524,31.232A4.887,4.887 0,0 1,150.638 36.119L150.638,36.119A4.887,4.887 0,0 1,145.751 31.232z"/>
+    <path android:fillColor="#FFE021" android:pathData="M145.751,139.164L145.751,124.292A4.887,4.887 0,0 1,150.638 119.405L150.638,119.405A4.887,4.887 0,0 1,155.524 124.292L155.524,139.164A4.887,4.887 0,0 1,150.638 144.051L150.638,144.051A4.887,4.887 0,0 1,145.751 139.164z"/>
+    <path android:fillColor="#FFE021" android:pathData="M102.889,117.23L113.405,106.713A4.887,4.887 45.169,0 1,120.316 106.713L120.316,106.713A4.887,4.887 45.169,0 1,120.316 113.624L109.8,124.14A4.887,4.887 45.169,0 1,102.889 124.14L102.889,124.14A4.887,4.887 45.169,0 1,102.889 117.23z"/>
+    <path android:fillColor="#FFE021" android:pathData="M178.951,42.017L189.468,31.501A4.887,4.887 90,0 1,196.379 31.501L196.379,31.501A4.887,4.887 90,0 1,196.379 38.412L185.862,48.928A4.887,4.887 0,0 1,178.951 48.928L178.951,48.928A4.887,4.887 0,0 1,178.951 42.017z"/>
+</vector>

+ 16 - 0
feature_measure/src/main/res/drawable/ic_edit.xml

@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="20"
+    android:viewportHeight="20">
+  <path
+      android:pathData="M3.3333,4.1667C3.1123,4.1667 2.9004,4.2545 2.7441,4.4107C2.5878,4.567 2.5,4.779 2.5,5V16.6667C2.5,16.8877 2.5878,17.0996 2.7441,17.2559C2.9004,17.4122 3.1123,17.5 3.3333,17.5H15C15.221,17.5 15.433,17.4122 15.5892,17.2559C15.7455,17.0996 15.8333,16.8877 15.8333,16.6667V12.2167C15.8333,11.7564 16.2064,11.3833 16.6667,11.3833C17.1269,11.3833 17.5,11.7564 17.5,12.2167V16.6667C17.5,17.3297 17.2366,17.9656 16.7678,18.4344C16.2989,18.9033 15.663,19.1667 15,19.1667H3.3333C2.6703,19.1667 2.0344,18.9033 1.5656,18.4344C1.0967,17.9656 0.8333,17.3297 0.8333,16.6667V5C0.8333,4.337 1.0967,3.7011 1.5656,3.2322C2.0344,2.7634 2.6703,2.5 3.3333,2.5H7.7833C8.2436,2.5 8.6167,2.8731 8.6167,3.3333C8.6167,3.7936 8.2436,4.1667 7.7833,4.1667H3.3333Z"
+      android:fillColor="#000000"
+      android:fillAlpha="0.5"
+      android:fillType="evenOdd"/>
+  <path
+      android:pathData="M14.4107,1.0775C14.7362,0.752 15.2638,0.752 15.5893,1.0775L18.9226,4.4108C19.248,4.7362 19.248,5.2639 18.9226,5.5893L10.5893,13.9226C10.433,14.0789 10.221,14.1667 10,14.1667H6.6667C6.2064,14.1667 5.8333,13.7936 5.8333,13.3334V10C5.8333,9.779 5.9211,9.5671 6.0774,9.4108L14.4107,1.0775ZM7.5,10.3452V12.5H9.6548L17.1548,5L15,2.8452L7.5,10.3452Z"
+      android:fillColor="#000000"
+      android:fillAlpha="0.5"
+      android:fillType="evenOdd"/>
+</vector>

+ 6 - 0
feature_measure/src/main/res/drawable/ic_evening.xml

@@ -0,0 +1,6 @@
+<vector android:height="55dp" android:viewportHeight="300"
+    android:viewportWidth="300" android:width="55dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF9170" android:pathData="M300,149.797C300,232.527 232.843,299.593 150,299.593C87.232,299.593 33.468,261.092 11.098,206.448C3.943,188.971 0,169.844 0,149.797C0,67.066 67.157,0 150,0C232.843,0 300,67.066 300,149.797Z"/>
+    <path android:fillColor="#FFD545" android:fillType="evenOdd" android:pathData="M154.72,165.383C146.296,183.014 128.226,195.202 107.295,195.202C86.248,195.202 68.096,182.88 59.732,165.094C63.076,163.678 65.014,161.997 65.014,160.193C65.014,157.794 61.585,155.611 55.978,153.982C55.217,150.444 54.816,146.772 54.816,143.007C54.816,136.99 55.84,131.211 57.723,125.832C66.973,124.204 73.088,121.386 73.088,118.182C73.088,115.907 70.005,113.827 64.906,112.229C74.452,99.245 89.883,90.812 107.295,90.812C125.02,90.812 140.692,99.552 150.194,112.935C146.324,114.419 144.051,116.229 144.051,118.182C144.051,121.058 148.977,123.623 156.675,125.295C158.68,130.825 159.773,136.789 159.773,143.007C159.773,146.682 159.391,150.269 158.665,153.73C152.525,155.382 148.725,157.668 148.725,160.193C148.725,162.121 150.94,163.91 154.72,165.383Z"/>
+    <path android:fillColor="#6D6B6C" android:pathData="M292.351,199.074C285.309,218.622 296.58,189.512 289.041,207.66C281.501,225.808 270.451,242.298 256.52,256.188C242.588,270.078 226.05,281.096 207.848,288.614C189.646,296.131 170.138,300 150.436,300C130.735,300 111.226,296.131 93.024,288.614C74.823,281.096 58.284,270.078 44.353,256.188C30.422,242.298 19.371,225.808 11.832,207.66C4.292,189.512 8.521,201.199 3.399,182.511C21.017,169.347 31.629,164.486 52.269,161.678C52.269,161.678 71.903,161.678 81.933,164.866C91.963,168.053 106.261,174.852 106.261,174.852C111.17,178.039 112.877,178.676 118.639,182.511C122.866,186.725 127.388,190.15 132.51,193.337C138.912,199.074 143.607,203.111 150.436,207.66C156.198,213.31 159.866,216.497 165.375,220.109C174.124,225.846 183.941,229.246 189.703,229.246C198.879,229.246 221.287,220.109 225.982,216.497C256.52,193.003 266.643,195.804 275.492,195.804L292.351,199.074Z"/>
+</vector>

+ 9 - 0
feature_measure/src/main/res/drawable/ic_morning.xml

@@ -0,0 +1,9 @@
+<vector android:height="55dp" android:viewportHeight="300"
+    android:viewportWidth="300" android:width="55dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#A8E2FA" android:pathData="M300,150C300,232.843 232.843,300 150,300C87.232,300 33.468,261.447 11.098,206.728C3.943,189.228 0,170.075 0,150C0,67.157 67.157,0 150,0C232.843,0 300,67.157 300,150Z"/>
+    <path android:fillColor="#B4A5FA" android:fillType="evenOdd" android:pathData="M288.467,207.365C265.737,261.294 212.381,299.15 150.183,299.15C87.985,299.15 34.628,261.294 11.898,207.365L288.467,207.365Z"/>
+    <path android:fillColor="#FFE694" android:fillType="evenOdd" android:pathData="M220.113,17.422L205.666,10.836V168.976C200.173,170.058 194.982,172.362 190.487,175.725L51.822,37.061C47.539,40.4 45.694,42.38 42.878,46.005L181.784,184.911C179.05,189.008 177.152,193.608 176.2,198.442H7.649L11.048,206.941H289.164L291.714,198.442H249.579C248.654,193.746 246.837,189.272 244.227,185.264L299.018,130.473L295.751,114.094L234.167,174.915C229.939,171.99 225.154,169.969 220.113,168.976V17.422Z"/>
+    <path android:fillColor="#FFFFFA" android:pathData="M204.391,216.289C215.483,224.118 221.034,228.659 229.783,237.004C209.033,225.7 197.936,225.968 178.999,237.004C187.05,228.61 192.46,224.05 204.391,216.289Z"/>
+    <path android:fillColor="#FFFFFA" android:pathData="M150.321,249.009C161.413,256.838 166.964,261.378 175.713,269.724C154.964,258.419 143.866,258.688 124.929,269.724C132.98,261.329 138.39,256.769 150.321,249.009Z"/>
+    <path android:fillColor="#FFFFFA" android:pathData="M95.93,216.289C107.022,224.118 112.573,228.659 121.322,237.004C100.573,225.7 89.476,225.968 70.538,237.004C78.589,228.61 84,224.05 95.93,216.289Z"/>
+</vector>

+ 7 - 0
feature_measure/src/main/res/drawable/ic_night.xml

@@ -0,0 +1,7 @@
+<vector android:height="55dp" android:viewportHeight="300"
+    android:viewportWidth="300" android:width="55dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#7373C9" android:pathData="M300,150C300,232.843 232.843,300 150,300C87.232,300 33.468,261.447 11.098,206.728C3.943,189.228 0,170.075 0,150C0,67.157 67.157,0 150,0C232.843,0 300,67.157 300,150Z"/>
+    <path android:fillColor="#FFF795" android:fillType="evenOdd" android:pathData="M147.533,233.709C181.797,233.709 209.573,205.838 209.573,171.457C209.573,153.406 201.916,137.15 189.684,125.779C223.714,126.053 251.216,153.818 251.216,188.029C251.216,222.41 223.44,250.281 189.176,250.281C172.902,250.281 158.091,243.994 147.025,233.707C147.195,233.708 147.364,233.709 147.533,233.709Z"/>
+    <path android:fillColor="#FFDF00" android:pathData="M75.637,119.405L83.505,135.347L101.098,137.903L88.368,150.312L91.373,167.833L75.637,159.561L59.902,167.833L62.907,150.312L50.177,137.903L67.77,135.347L75.637,119.405Z"/>
+    <path android:fillColor="#FFDF00" android:pathData="M140.652,38.244L150.268,57.601L171.77,60.706L156.211,75.773L159.884,97.049L140.652,87.004L121.42,97.049L125.093,75.773L109.534,60.706L131.036,57.601L140.652,38.244Z"/>
+</vector>

+ 5 - 0
feature_measure/src/main/res/drawable/ic_refresh_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
+</vector>

+ 120 - 0
feature_measure/src/main/res/layout/fragment_edit_measure.xml

@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/buttonChooseDate"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="32dp"
+                android:layout_marginEnd="8dp"
+                android:text="@string/choose_date"
+                app:layout_constraintEnd_toStartOf="@+id/buttonChooseTime"
+                app:layout_constraintHorizontal_bias="0.5"
+                app:layout_constraintHorizontal_chainStyle="spread"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/buttonChooseTime"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:layout_marginTop="32dp"
+                android:layout_marginEnd="16dp"
+                android:text="@string/choose_time"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="0.5"
+                app:layout_constraintStart_toEndOf="@+id/buttonChooseDate"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <TextView
+                android:id="@+id/tvMeasureAddedDate"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:layout_marginTop="8dp"
+                android:layout_marginEnd="8dp"
+                android:text="пн, 30 нояб. 2020 22:11:29"
+                android:textAlignment="center"
+                android:textAppearance="@style/TextAppearance.AppCompat.Large"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/buttonChooseDate" />
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/tilMeasureValue"
+                style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="16dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tvMeasureAddedDate">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:digits="0123456789*./"
+                    android:inputType="date"
+                    android:singleLine="true" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/tilComment"
+                style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="32dp"
+                android:layout_marginEnd="16dp"
+                app:endIconMode="clear_text"
+                android:hint="@string/ivMeasureComment"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/tilMeasureValue">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:inputType="textMultiLine"
+                    android:maxLength="150" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+
+            <ProgressBar
+                android:id="@+id/progressBar"
+                style="?android:attr/progressBarStyle"
+                android:layout_width="128dp"
+                android:layout_height="128dp"
+                android:indeterminate="true"
+                android:visibility="invisible"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:visibility="visible" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </ScrollView>
+</layout>

+ 55 - 0
feature_measure/src/main/res/layout/fragment_list_measure.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.google.android.material.floatingactionbutton.FloatingActionButton
+            android:id="@+id/fabAddMeasure"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="32dp"
+            android:layout_marginBottom="32dp"
+            android:clickable="true"
+            android:focusable="true"
+            app:fabSize="normal"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:srcCompat="@drawable/ic_add_24"
+            app:useCompatPadding="true"
+            tools:ignore="ContentDescription" />
+
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rvMeasure"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="59dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:listitem="@layout/item_measure" />
+
+        <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyle"
+            android:layout_width="128dp"
+            android:layout_height="128dp"
+            android:indeterminate="true"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:visibility="visible" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</layout>

+ 54 - 0
feature_measure/src/main/res/layout/fragment_tab_measure.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.google.android.material.tabs.TabLayout
+            android:id="@+id/measureTypesTabs"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/design_default_color_primary"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:tabIconTint="@color/tab_color_selector"
+            app:tabIndicatorColor="@color/white"
+            app:tabMaxWidth="0dp"
+            app:tabGravity="fill"
+            app:tabMode="fixed">
+
+        </com.google.android.material.tabs.TabLayout>
+
+        <androidx.viewpager2.widget.ViewPager2
+            android:id="@+id/viewpager"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/measureTypesTabs" />
+
+        <ProgressBar
+            android:id="@+id/progressBar"
+            style="?android:attr/progressBarStyle"
+            android:layout_width="128dp"
+            android:layout_height="128dp"
+            android:indeterminate="true"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:visibility="visible" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</layout>

+ 101 - 0
feature_measure/src/main/res/layout/item_measure.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <data>
+        <variable
+            name="measure"
+            type="com.mrozon.core_api.entity.Measure" />
+        <variable
+            name="measureType"
+            type="com.mrozon.core_api.entity.MeasureType" />
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/layoutMeasure">
+
+        <ImageView
+            android:id="@+id/ivTimeOfDay"
+            android:layout_width="60dp"
+            android:layout_height="60dp"
+            android:layout_marginStart="16dp"
+            android:layout_marginLeft="16dp"
+            android:layout_marginTop="8dp"
+            android:layout_marginBottom="8dp"
+            android:contentDescription="@string/ivTimeOfDay"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:time_of_day="@{measure}"
+            app:srcCompat="@drawable/ic_day" />
+
+
+        <ImageView
+            android:id="@+id/ivMeasureComment"
+            android:layout_width="20dp"
+            android:layout_height="20dp"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="8dp"
+            android:contentDescription="@string/ivMeasureComment"
+            app:layout_constraintBottom_toTopOf="@+id/tvMeasureValue"
+            app:layout_constraintStart_toEndOf="@+id/ivTimeOfDay"
+            app:layout_constraintTop_toTopOf="parent"
+            app:srcCompat="@drawable/ic_edit" />
+
+        <TextView
+            android:id="@+id/tvMeasureValue"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_marginEnd="8dp"
+            android:gravity="center"
+            tools:text="120/70 мм рт.ст."
+            app:measure="@{measure}"
+            app:measure_type="@{measureType}"
+            android:textAlignment="center"
+            android:textColor="#000000"
+            android:textSize="24sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/ivTimeOfDay"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:ignore="UnknownId" />
+
+        <TextView
+            android:id="@+id/tvMeasureDate"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_marginEnd="16dp"
+            android:layout_marginRight="16dp"
+            android:layout_marginBottom="2dp"
+            android:gravity="center"
+            app:added_date="@{measure}"
+            android:textAlignment="center"
+            android:textColor="#99000000"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/tvMeasureValue"
+            tools:text="27 мар. 17:40" />
+
+        <TextView
+            android:id="@+id/tvMeasureComment"
+            android:layout_width="0dp"
+            android:layout_height="20dp"
+            android:layout_marginStart="8dp"
+            android:layout_marginLeft="8dp"
+            android:layout_marginEnd="8dp"
+            android:layout_marginRight="8dp"
+            android:text="@{measure.comment}"
+            tools:text="Это комментарий"
+            app:layout_constraintBottom_toTopOf="@+id/tvMeasureValue"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@+id/ivMeasureComment" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</layout>

+ 16 - 0
feature_measure/src/main/res/menu/add_measure_menu.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/deleteMeasure"
+        android:icon="@drawable/ic_baseline_delete_outline_24"
+        android:title="@string/delete_measure"
+        android:visible="false"
+        app:showAsAction="ifRoom" />
+
+    <item
+        android:id="@+id/saveMeasure"
+        android:icon="@drawable/ic_check_white_24dp"
+        android:title="@string/save_measure"
+        app:showAsAction="always" />
+</menu>

+ 11 - 0
feature_measure/src/main/res/menu/list_measure_menu.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/refreshMeasuresNetwork"
+        android:icon="@drawable/ic_refresh_24"
+        android:title="@string/refresh"
+        app:showAsAction="ifRoom" />
+
+</menu>

+ 4 - 0
feature_measure/src/main/res/values/colors.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="white">#FFFFFF</color>
+</resources>

+ 15 - 0
feature_measure/src/main/res/values/strings.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="ivTimeOfDay">Time of day</string>
+    <string name="ivMeasureComment">Comment</string>
+    <string name="refresh">Refresh</string>
+    <string name="add_measure">Add measure</string>
+    <string name="delete_measure">Delete measure</string>
+    <string name="save_measure">Save measure</string>
+    <string name="choose_date">Choose date</string>
+    <string name="choose_time">Choose Time</string>
+    <string name="measure_added">Measure added</string>
+    <string name="hint_value_added">%s ( in %s)</string>
+    <string name="error_value">Incorrect value, needed %s</string>
+    <string name="edit_measure">Edit measure</string>
+</resources>

+ 17 - 0
feature_measure/src/test/java/com/mrozon/feature_measure/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.mrozon.feature_measure
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 1 - 2
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt

@@ -65,8 +65,7 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
         adapter = ListPersonAdapter(object : ListPersonAdapter.ListPersonClickListener {
             override fun onClick(person: Person) {
                 Timber.d("click to ${person.name}")
-                //TODO add logic for click item
-                show("will be soon))")
+                navigator.navigateToMeasureForPerson(findNavController(), person.id)
             }
 
             override fun onLongClick(person: Person) {

+ 3 - 1
scripts/deps_versions.gradle

@@ -11,7 +11,7 @@ ext {
     constraintlayoutVersion = '1.1.3'
     cardviewVersion = '1.0.0'
     recyclerviewViersion = '1.2.0-alpha05'
-    materialVersion = '1.2.1'
+    materialVersion = '1.3.0-alpha04'
     lifecycleVersion = '2.3.0-alpha07'
     junitVersion = "4.12"
     mockitoCoreVersion = "2.28.2"
@@ -26,6 +26,7 @@ ext {
     androidXTestVersion = "1.1.0"
     securityCryptoVersion = "1.1.0-alpha02"
     coilVersion = "1.0.0"
+    viewpager2Version = "1.0.0"
 
     // DI
     dagger = "com.google.dagger:dagger:$daggerVersion"
@@ -53,6 +54,7 @@ ext {
     cardview =  "androidx.cardview:cardview:$cardviewVersion"
     recyclerview = "androidx.recyclerview:recyclerview:$recyclerviewViersion"
     material = "com.google.android.material:material:$materialVersion"
+    viewpager2 =  "androidx.viewpager2:viewpager2:$viewpager2Version"
     // Livecycle
     lifecycleExtensions = "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
     lifecycleLivedata = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"

+ 1 - 0
settings.gradle

@@ -1,4 +1,5 @@
 include ':lint_repo'
+include ':feature_measure'
 include ':feature_measure_type'
 include ':feature_person'
 include ':feature_auth'

+ 6 - 0
utils/src/main/java/com/mrozon/utils/extension/FragmentExt.kt

@@ -24,6 +24,12 @@ fun Fragment.hideKeyboard() {
     }
 }
 
+fun Fragment.setTitleActionBar(title: String){
+    if(activity is AppCompatActivity){
+        (activity as AppCompatActivity).supportActionBar?.title = title
+    }
+}
+
 fun Fragment.isActiveNetwork(): Boolean {
     val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
     val activeNetwork: NetworkInfo? = cm.activeNetworkInfo