Add a yeary stats screen

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-09-23 14:16:31 +02:00
parent 6bb58e06c0
commit 3645b92a82
24 changed files with 744 additions and 464 deletions

View file

@ -8,15 +8,15 @@
<SelectionState runConfigName="EventFactoryText"> <SelectionState runConfigName="EventFactoryText">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="text2Pills_3()">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="EventEditPreview"> <SelectionState runConfigName="EventEditPreview">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="YearPreview"> <SelectionState runConfigName="YearPreview">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
<SelectionState runConfigName="ReportBoxPreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

View file

@ -47,9 +47,9 @@ class EventRepository @Inject constructor(
) )
// Collectable data. Provide event data in a nested map struct of year, month, day. // Collectable data. Provide event data in a nested map struct of year, month, day.
private val eventMapFlow: StateFlow<Map<Int, Map<Int, Map<Int, Event>>>> = eventListFlow private val eventMapFlow: StateFlow<Map<Int, Map<Int, Map<Int, List<Event>>>>> = eventListFlow
.map { events -> .map { events ->
events.fold(initial = hashMapOf<Int, HashMap<Int, HashMap<Int, Event>>>()) { acc, event -> events.fold(initial = hashMapOf<Int, HashMap<Int, HashMap<Int, MutableList<Event>>>>()) { acc, event ->
acc.also { acc.also {
val years = it.getOrPut(key = event.date.year) { val years = it.getOrPut(key = event.date.year) {
hashMapOf( hashMapOf(
@ -68,7 +68,8 @@ class EventRepository @Inject constructor(
) )
} }
val months = years.getOrPut(key = event.date.month) { hashMapOf() } val months = years.getOrPut(key = event.date.month) { hashMapOf() }
months[event.date.day] = event val events = months.getOrPut(key = event.date.day) { mutableListOf() }
events.add(event)
} }
} }
} }
@ -78,6 +79,20 @@ class EventRepository @Inject constructor(
initialValue = emptyMap(), initialValue = emptyMap(),
) )
private val maxPillAmountPerMonth: StateFlow<Int> = eventMapFlow
.map { events ->
events.values.maxOf { yearEntry: Map<Int, Map<Int, List<Event>>> ->
yearEntry.values.maxOf { monthEntry ->
monthEntry.values.sumOf { events -> events.sumOf { it.pills.size } }
}
}
}
.stateIn(
scope = scope,
started = SharingStarted.Lazily,
initialValue = 0,
)
init { init {
calendarIdRepository.calendarId calendarIdRepository.calendarId
.onEach { id -> .onEach { id ->
@ -92,7 +107,9 @@ class EventRepository @Inject constructor(
fun eventsListFlow(): StateFlow<Collection<Event>> = eventListFlow fun eventsListFlow(): StateFlow<Collection<Event>> = eventListFlow
fun eventsMapFlow(): StateFlow<Map<Int, Map<Int, Map<Int, Event>>>> = eventMapFlow fun eventsMapFlow(): StateFlow<Map<Int, Map<Int, Map<Int, List<Event>>>>> = eventMapFlow
fun maxPillAmountPerMonthFlow(): StateFlow<Int> = maxPillAmountPerMonth
fun event(id: Long): Event? = eventFlow.value.get(key = id) fun event(id: Long): Event? = eventFlow.value.get(key = id)

View file

@ -0,0 +1,19 @@
package com.pixelized.headache.ui.navigation.destination
import androidx.navigation3.runtime.EntryProviderBuilder
import androidx.navigation3.runtime.entry
import com.pixelized.headache.ui.navigation.home.HomeDestination
import com.pixelized.headache.ui.navigation.home.HomeNavigator
import com.pixelized.headache.ui.page.summary.report.ReportPage
data object ReportDestination : HomeDestination
fun EntryProviderBuilder<*>.reportDestinationEntry() {
entry<ReportDestination> {
ReportPage()
}
}
fun HomeNavigator.navigateToReport() {
goTo(ReportDestination)
}

View file

@ -13,6 +13,7 @@ import com.pixelized.headache.ui.navigation.destination.calendarChooserDestinati
import com.pixelized.headache.ui.navigation.destination.eventDestinationEntry import com.pixelized.headache.ui.navigation.destination.eventDestinationEntry
import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry
import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry
import com.pixelized.headache.ui.navigation.destination.reportDestinationEntry
import com.pixelized.headache.ui.navigation.destination.yearSummaryDestinationEntry import com.pixelized.headache.ui.navigation.destination.yearSummaryDestinationEntry
val LocalHomeNavigator = staticCompositionLocalOf<HomeNavigator> { val LocalHomeNavigator = staticCompositionLocalOf<HomeNavigator> {
@ -41,6 +42,7 @@ fun HomeNavDisplay(
entryProvider = entryProvider { entryProvider = entryProvider {
monthSummaryDestinationEntry() monthSummaryDestinationEntry()
yearSummaryDestinationEntry() yearSummaryDestinationEntry()
reportDestinationEntry()
} }
) )
} }

View file

@ -67,13 +67,13 @@ class EventEditFactory @Inject constructor() {
EventPillEditUio( EventPillEditUio(
id = Event.Pill.Id.IBUPROFENE_400.value, id = Event.Pill.Id.IBUPROFENE_400.value,
color = HeadacheColorPalette.Pill.Ibuprofene400, color = HeadacheColorPalette.Pill.Ibuprofene400,
label = "Paracétamol 1000", label = "Ibuprofène 400",
amount = ibuprofeneAmount, amount = ibuprofeneAmount,
), ),
EventPillEditUio( EventPillEditUio(
id = Event.Pill.Id.PARACETAMOL_1000.value, id = Event.Pill.Id.PARACETAMOL_1000.value,
color = HeadacheColorPalette.Pill.Paracetamol1000, color = HeadacheColorPalette.Pill.Paracetamol1000,
label = "Ibuprofène 400", label = "Paracétamol 1000",
amount = paracetamolAmount, amount = paracetamolAmount,
), ),
EventPillEditUio( EventPillEditUio(

View file

@ -21,26 +21,26 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.IntState
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.headache.R import com.pixelized.headache.R
import com.pixelized.headache.ui.navigation.destination.MonthSummaryDestination
import com.pixelized.headache.ui.navigation.destination.ReportDestination
import com.pixelized.headache.ui.navigation.destination.YearSummaryDestination
import com.pixelized.headache.ui.navigation.destination.navigateToMonthSummary import com.pixelized.headache.ui.navigation.destination.navigateToMonthSummary
import com.pixelized.headache.ui.navigation.destination.navigateToReport
import com.pixelized.headache.ui.navigation.destination.navigateToYearSummary import com.pixelized.headache.ui.navigation.destination.navigateToYearSummary
import com.pixelized.headache.ui.navigation.home.HomeNavDisplay import com.pixelized.headache.ui.navigation.home.HomeNavDisplay
import com.pixelized.headache.ui.navigation.home.HomeNavigator import com.pixelized.headache.ui.navigation.home.HomeNavigator
@ -61,18 +61,25 @@ fun HomePage(
navigator: HomeNavigator, navigator: HomeNavigator,
editViewModel: EventEditBottomSheetViewModel = hiltViewModel(), editViewModel: EventEditBottomSheetViewModel = hiltViewModel(),
) { ) {
val selectedItem = remember { mutableIntStateOf(0) } val selectedItem = remember {
derivedStateOf {
when (navigator.backStack.last()) {
is YearSummaryDestination -> 0
is ReportDestination -> 1
is MonthSummaryDestination -> 2
else -> -1
}
}
}
val items = rememberBottomBarItems( val items = rememberBottomBarItems(
onYearlyFollowUp = { onYearlyFollowUp = {
selectedItem.intValue = 0
navigator.navigateToYearSummary() navigator.navigateToYearSummary()
}, },
onMonthlyStatFollowUp = { onMonthlyStatFollowUp = {
selectedItem.intValue = 1 navigator.navigateToReport()
navigator.navigateToMonthSummary()
}, },
onMonthlyListFollowUp = { onMonthlyListFollowUp = {
selectedItem.intValue = 2 navigator.navigateToMonthSummary()
}, },
) )
@ -123,7 +130,7 @@ private fun HomePageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navigator: HomeNavigator, navigator: HomeNavigator,
items: List<BottomBarItemUio>, items: List<BottomBarItemUio>,
selectedItem: IntState, selectedItem: State<Int>,
onFabClick: () -> Unit, onFabClick: () -> Unit,
) { ) {
Scaffold( Scaffold(

View file

@ -1,44 +1,43 @@
package com.pixelized.headache.ui.page.summary.monthly package com.pixelized.headache.ui.page.summary.monthly
import android.icu.text.Collator
import android.icu.util.Calendar import android.icu.util.Calendar
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.intl.Locale
import com.pixelized.headache.repository.event.Event import com.pixelized.headache.repository.event.Event
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBoxUio
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryTitleUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryTitleUio
import com.pixelized.headache.utils.extention.event
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max
class MonthSummaryFactory @Inject constructor() { class MonthSummaryFactory @Inject constructor() {
private val calendar = Calendar.getInstance() private val calendar = Calendar.getInstance()
private val locale = Locale.current.let {
java.util.Locale.forLanguageTag(it.toLanguageTag())
}
fun convertToItemUio( fun convertToItemUio(
events: Collection<Event>, events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
): Map<MonthSummaryCell, List<MonthSummaryCell>> { ): Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>> {
return events return events
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event -> .flatMap { yearEntry ->
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) } yearEntry.value.map { monthEntry ->
} val pills = monthEntry.value.values
.map { entry -> .fold(
val pills = entry.value initial = hashMapOf<Event.Pill.Id, PillFolder>(),
.fold(hashMapOf<Event.Pill.Id, EventFolder>()) { acc, event -> ) { acc, events ->
events.map { event ->
event.pills.forEach { pill -> event.pills.forEach { pill ->
val value = acc.getOrElse( val value = acc.getOrElse(
key = pill.id, key = pill.id,
defaultValue = { defaultValue = {
EventFolder( PillFolder(label = pill.label, color = pill.color)
label = pill.label,
amount = 0,
color = pill.color,
)
}, },
) )
value.amount += 1 value.amount += pill.amount
acc[pill.id] = value acc[pill.id] = value
} }
}
acc acc
} }
.map { entry -> .map { entry ->
@ -49,58 +48,31 @@ class MonthSummaryFactory @Inject constructor() {
) )
} }
.toList() .toList()
.sortedWith(Comparator { s1, s2 ->
Collator.getInstance(locale).compare(s1.label, s2.label)
})
MonthSummaryItemUio( MonthSummaryItemUio(
date = calendar.apply { event = entry.key }.time, date = calendar.apply {
days = entry.value.size, set(Calendar.YEAR, yearEntry.key)
set(Calendar.MONTH, monthEntry.key)
}.time,
days = monthEntry.value.values.sumOf { it.size },
pills = pills, pills = pills,
) )
} }
.sortedByDescending { it.date } }
.groupByMonth() .groupByMonth()
.toSortedMap{s1, s2 ->
when{
s1.date < s2.date -> 1
s1.date > s2.date -> -1
else -> 0
}
}
} }
fun convertToBoxUio( fun List<MonthSummaryItemUio>.groupByMonth(): Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>> {
events: Collection<Event>,
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
var maxPillAmount = 0
return events
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) }
}
.mapKeys { entry ->
maxPillAmount = max(
entry.value.sumOf { events -> events.pills.sumOf { pill -> pill.amount } },
maxPillAmount,
)
entry.key
}
.map { entry ->
val pillAmount = entry.value.sumOf { events ->
events.pills.sumOf { pill -> pill.amount }
}
val monthMaxDay = calendar.apply {
event = entry.key
add(Calendar.MONTH, 1)
add(Calendar.DAY_OF_YEAR, -1)
}.get(Calendar.DAY_OF_MONTH)
MonthSummaryBoxUio(
date = calendar.apply { event = entry.key }.time,
headacheRatio = entry.value.size.toFloat() / monthMaxDay,
headacheAmount = entry.value.size,
headacheColor = Color.Companion.Red,
pillRatio = pillAmount.takeIf { it > 0 }
?.let { it.toFloat() / maxPillAmount.toFloat() },
pillAmount = pillAmount,
pillColor = Color.Companion.Blue,
)
}
.sortedByDescending { it.date }
.groupByMonth()
}
fun List<MonthSummaryCell>.groupByMonth(): Map<MonthSummaryCell, List<MonthSummaryCell>> {
return this.groupBy { return this.groupBy {
MonthSummaryTitleUio( MonthSummaryTitleUio(
date = calendar.apply { date = calendar.apply {
@ -112,9 +84,9 @@ class MonthSummaryFactory @Inject constructor() {
} }
} }
private class EventFolder( private class PillFolder(
val label: String, val label: String,
val color: Color, val color: Color,
var amount: Int, var amount: Int = 0,
) )
} }

View file

@ -1,23 +1,13 @@
package com.pixelized.headache.ui.page.summary.monthly package com.pixelized.headache.ui.page.summary.monthly
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Article
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -29,7 +19,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.keepScreenOn import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -40,9 +29,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.headache.R import com.pixelized.headache.R
import com.pixelized.headache.ui.navigation.destination.navigateToEventPage import com.pixelized.headache.ui.navigation.destination.navigateToEventPage
import com.pixelized.headache.ui.navigation.main.LocalMainNavigator import com.pixelized.headache.ui.navigation.main.LocalMainNavigator
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBox
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBoxUio
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItem import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItem
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio
@ -69,7 +55,6 @@ fun MonthSummaryPage(
viewModel: MonthSummaryViewModel = hiltViewModel(), viewModel: MonthSummaryViewModel = hiltViewModel(),
) { ) {
val navigation = LocalMainNavigator.current val navigation = LocalMainNavigator.current
val boxMode = viewModel.boxMode.collectAsStateWithLifecycle()
val events = viewModel.events.collectAsStateWithLifecycle() val events = viewModel.events.collectAsStateWithLifecycle()
MonthSummaryContent( MonthSummaryContent(
@ -77,10 +62,6 @@ fun MonthSummaryPage(
.keepScreenOn() .keepScreenOn()
.fillMaxSize(), .fillMaxSize(),
events = events, events = events,
boxMode = boxMode,
onDisplay = {
viewModel.toggleDisplay()
},
onItem = { onItem = {
navigation.navigateToEventPage(date = it.date) navigation.navigateToEventPage(date = it.date)
}, },
@ -93,10 +74,8 @@ private fun MonthSummaryContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp = MonthSummaryPageDefault.spacing, spacing: Dp = MonthSummaryPageDefault.spacing,
listPadding: PaddingValues = MonthSummaryPageDefault.listPadding, listPadding: PaddingValues = MonthSummaryPageDefault.listPadding,
boxMode: State<Boolean>, events: State<Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>>>,
events: State<Map<MonthSummaryCell, List<MonthSummaryCell>>>, onItem: (MonthSummaryItemUio) -> Unit,
onDisplay: () -> Unit,
onItem: (MonthSummaryCell) -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@ -109,24 +88,6 @@ private fun MonthSummaryContent(
title = { title = {
Text(text = stringResource(R.string.month_summary_title)) Text(text = stringResource(R.string.month_summary_title))
}, },
actions = {
AnimatedContent(
targetState = boxMode.value,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) {
IconButton(
onClick = onDisplay,
) {
Icon(
imageVector = when (it) {
true -> Icons.AutoMirrored.Filled.Article
else -> Icons.Default.BarChart
},
contentDescription = null,
)
}
}
}
) )
}, },
content = { paddingValues -> content = { paddingValues ->
@ -140,24 +101,16 @@ private fun MonthSummaryContent(
) { ) {
events.value.forEach { entry -> events.value.forEach { entry ->
item { item {
MonthSummaryCell( MonthSummaryTitle(
modifier = Modifier.padding(top = 16.dp), modifier = Modifier.padding(top = 16.dp),
item = entry.key, item = entry.key,
onItem = onItem,
) )
} }
items( items(
items = entry.value, items = entry.value,
key = { item -> item.date }, key = { item -> item.date },
contentType = { item ->
when (item) {
is MonthSummaryBoxUio -> "MonthSummaryBoxUio"
is MonthSummaryItemUio -> "MonthSummaryItemUio"
is MonthSummaryTitleUio -> "MonthSummaryTitleUio"
}
},
) { item -> ) { item ->
MonthSummaryCell( MonthSummaryItem(
item = item, item = item,
onItem = onItem, onItem = onItem,
) )
@ -168,44 +121,11 @@ private fun MonthSummaryContent(
) )
} }
@Composable
private fun MonthSummaryCell(
modifier: Modifier = Modifier,
item: MonthSummaryCell,
onItem: (MonthSummaryCell) -> Unit,
) {
AnimatedContent(
modifier = Modifier
.fillMaxWidth()
.then(other = modifier),
targetState = item,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { item ->
when (item) {
is MonthSummaryTitleUio -> MonthSummaryTitle(
item = item,
)
is MonthSummaryBoxUio -> MonthSummaryBox(
item = item,
onItem = onItem,
)
is MonthSummaryItemUio -> MonthSummaryItem(
item = item,
onItem = onItem,
)
}
}
}
@Composable @Composable
@Preview @Preview
private fun MonthSummaryPreview() { private fun MonthSummaryPreview() {
HeadacheTheme { HeadacheTheme {
MonthSummaryContent( val events = remember {
boxMode = remember { mutableStateOf(false) },
events = remember {
mutableStateOf( mutableStateOf(
mapOf( mapOf(
MonthSummaryTitleUio( MonthSummaryTitleUio(
@ -227,20 +147,12 @@ private fun MonthSummaryPreview() {
), ),
), ),
), ),
MonthSummaryBoxUio(
date = Date(),
headacheRatio = 8f / 30f,
headacheAmount = 8,
headacheColor = Color.Red,
pillRatio = 6f / 20f,
pillAmount = 6,
pillColor = Color.Blue,
),
), ),
) )
) )
}, }
onDisplay = { }, MonthSummaryContent(
events = events,
onItem = { }, onItem = { },
) )
} }

View file

@ -2,15 +2,11 @@ package com.pixelized.headache.ui.page.summary.monthly
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.headache.repository.event.Event
import com.pixelized.headache.repository.event.EventRepository import com.pixelized.headache.repository.event.EventRepository
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject import javax.inject.Inject
@ -20,26 +16,13 @@ class MonthSummaryViewModel @Inject constructor(
eventItemFactory: MonthSummaryFactory, eventItemFactory: MonthSummaryFactory,
) : ViewModel() { ) : ViewModel() {
private val displayTypeFlow = MutableStateFlow(false)
val boxMode: StateFlow<Boolean> = displayTypeFlow
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val events: StateFlow<Map<MonthSummaryCell, List<MonthSummaryCell>>> = combine( val events = eventRepository.eventsMapFlow()
eventRepository.eventsListFlow(), .map { events ->
displayTypeFlow, eventItemFactory.convertToItemUio(events = events)
transform = { events: Collection<Event>, display -> }.stateIn(
when (display) {
true -> eventItemFactory.convertToBoxUio(events = events)
else -> eventItemFactory.convertToItemUio(events = events)
}
}
).stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
initialValue = emptyMap(), initialValue = emptyMap(),
) )
fun toggleDisplay() {
displayTypeFlow.value = displayTypeFlow.value.not()
}
} }

View file

@ -1,174 +0,0 @@
package com.pixelized.headache.ui.page.summary.monthly.item
import android.annotation.SuppressLint
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.headache.ui.theme.HeadacheTheme
import com.pixelized.headache.utils.extention.capitalize
import java.util.Date
import java.util.Locale
@Stable
data object MonthSummaryBoxDefault {
@Stable
val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
@Stable
val spacing: DpSize = DpSize(width = 8.dp, height = 4.dp)
@Stable
val labelWidth: Dp = 32.dp + 16.dp
@Stable
val boxHeight: Dp = 16.dp
@Stable
val boxPaddingValues: PaddingValues = PaddingValues(horizontal = 4.dp)
@SuppressLint("ConstantLocale")
@Stable
val formatter = SimpleDateFormat("MMM", Locale.getDefault())
}
@Stable
data class MonthSummaryBoxUio(
override val date: Date,
val headacheRatio: Float,
val headacheAmount: Int,
val headacheColor: Color,
val pillRatio: Float?,
val pillAmount: Int,
val pillColor: Color,
) : MonthSummaryCell
@Composable
fun MonthSummaryBox(
modifier: Modifier = Modifier,
padding: PaddingValues = MonthSummaryBoxDefault.padding,
spacing: DpSize = MonthSummaryBoxDefault.spacing,
boxPaddingValues: PaddingValues = MonthSummaryBoxDefault.boxPaddingValues,
labelWidth: Dp = MonthSummaryBoxDefault.labelWidth,
boxHeight: Dp = MonthSummaryBoxDefault.boxHeight,
formatter: SimpleDateFormat = MonthSummaryBoxDefault.formatter,
item: MonthSummaryBoxUio,
onItem: (MonthSummaryBoxUio) -> Unit,
) {
Row(
modifier = Modifier
.clickable { onItem(item) }
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = spacing.width),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.width(width = labelWidth),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.End,
text = formatter.format(item.date).capitalize(),
)
Column(
verticalArrangement = Arrangement.spacedBy(space = spacing.height),
) {
Box(
modifier = Modifier
.background(color = item.headacheColor)
.height(height = boxHeight)
.fillMaxWidth(fraction = item.headacheRatio)
.padding(paddingValues = boxPaddingValues),
contentAlignment = Alignment.CenterStart,
) {
Text(
style = MaterialTheme.typography.labelSmall,
color = Color.White,
text = "${item.headacheAmount}"
)
}
item.pillRatio?.let {
Box(
modifier = Modifier
.background(color = item.pillColor)
.height(height = boxHeight)
.fillMaxWidth(fraction = it)
.padding(paddingValues = boxPaddingValues),
) {
Text(
style = MaterialTheme.typography.labelSmall,
color = Color.White,
text = "${item.pillAmount}"
)
}
}
}
}
}
@Composable
@Preview
private fun MonthSummaryBoxPreview(
@PreviewParameter(BoxPreviewProvider::class) preview: MonthSummaryBoxUio,
) {
HeadacheTheme {
Surface {
MonthSummaryBox(
modifier = Modifier.fillMaxWidth(),
item = preview,
onItem = { },
)
}
}
}
private class BoxPreviewProvider : PreviewParameterProvider<MonthSummaryBoxUio> {
val calendar = Calendar.getInstance().apply {
time = Date()
set(Calendar.DAY_OF_MONTH, 1)
}
override val values: Sequence<MonthSummaryBoxUio>
get() = sequenceOf(
MonthSummaryBoxUio(
date = calendar.apply { set(Calendar.MONTH, Calendar.DECEMBER) }.time,
headacheRatio = 0.2f,
headacheAmount = 1,
headacheColor = Color.Red,
pillRatio = 0.3f,
pillAmount = 1,
pillColor = Color.Blue,
),
MonthSummaryBoxUio(
date = calendar.apply { set(Calendar.MONTH, Calendar.SEPTEMBER) }.time,
headacheRatio = 1f,
headacheAmount = 1,
headacheColor = Color.Red,
pillRatio = 0.3f,
pillAmount = 1,
pillColor = Color.Blue,
),
)
}

View file

@ -1,9 +0,0 @@
package com.pixelized.headache.ui.page.summary.monthly.item
import androidx.compose.runtime.Stable
import java.util.Date
@Stable
sealed interface MonthSummaryCell {
val date: Date
}

View file

@ -29,10 +29,10 @@ import java.util.Locale
@Stable @Stable
data class MonthSummaryItemUio( data class MonthSummaryItemUio(
override val date: Date, val date: Date,
val days: Int, val days: Int,
val pills: List<MonthSummaryPillItemUio>, val pills: List<MonthSummaryPillItemUio>,
) : MonthSummaryCell )
@Stable @Stable
object MonthSummaryItemDefault { object MonthSummaryItemDefault {

View file

@ -15,8 +15,8 @@ import java.util.Locale
@Stable @Stable
data class MonthSummaryTitleUio( data class MonthSummaryTitleUio(
override val date: Date, val date: Date,
) : MonthSummaryCell )
@Stable @Stable
object MonthSummaryTitleDefault { object MonthSummaryTitleDefault {

View file

@ -0,0 +1,229 @@
package com.pixelized.headache.ui.page.summary.report
import android.annotation.SuppressLint
import android.icu.text.DateFormat
import android.icu.text.SimpleDateFormat
import android.icu.util.Calendar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.headache.ui.theme.HeadacheTheme
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
import com.pixelized.headache.utils.extention.capitalize
import java.util.Date
import java.util.Locale
@Stable
class ReportBoxUio(
val year: Int,
val months: List<Month>,
) {
@Stable
data class Month(
val date: Date,
val stats: List<Bar>,
)
@Stable
data class Bar(
val color: Color,
val label: String,
val ratio: Float,
)
}
@Stable
object ReportBoxDefault {
@Stable
val barSpace: Dp = 4.dp
@Stable
val titleSpace: Dp = 8.dp
@Stable
val barSize: DpSize = DpSize(
width = 28.dp,
height = 320.dp,
)
@Stable
val barShape: Shape = RoundedCornerShape(
topStart = barSize.width / 4,
topEnd = barSize.width / 4
)
@SuppressLint("ConstantLocale")
@Stable
val formatter = SimpleDateFormat("MMM", Locale.getDefault())
}
@Composable
fun ReportBox(
modifier: Modifier = Modifier,
formatter: DateFormat = ReportBoxDefault.formatter,
barSize: DpSize = ReportBoxDefault.barSize,
barShape: Shape = ReportBoxDefault.barShape,
barSpace: Dp = ReportBoxDefault.barSpace,
titleSpace: Dp = ReportBoxDefault.titleSpace,
item: ReportBoxUio,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = titleSpace),
) {
Text(
modifier = Modifier.padding(top = 16.dp),
style = MaterialTheme.typography.displaySmall,
text = "${item.year}",
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = barSpace),
) {
item.months.forEach {
Month(
formatter = formatter,
barSize = barSize,
barShape = barShape,
item = it,
)
}
}
}
}
@Composable
private fun Month(
modifier: Modifier = Modifier,
formatter: DateFormat = ReportBoxDefault.formatter,
barSize: DpSize = ReportBoxDefault.barSize,
barShape: Shape = ReportBoxDefault.barShape,
item: ReportBoxUio.Month,
) {
Column(
modifier = Modifier
.width(width = barSize.width)
.then(other = modifier),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier.height(height = barSize.height),
contentAlignment = Alignment.BottomStart,
) {
item.stats.forEachIndexed { index, stat ->
Box(
modifier = Modifier
.size(
width = barSize.width,
height = barSize.height * stat.ratio
)
.background(
color = stat.color,
shape = barShape,
),
contentAlignment = Alignment.TopCenter,
) {
Text(
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
color = Color.White,
text = stat.label,
)
}
}
}
Text(
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = formatter.format(item.date).capitalize(),
)
}
}
@Composable
@Preview
private fun ReportBoxPreview(
@PreviewParameter(ReportBoxPreviewProvider::class) preview: ReportBoxUio,
) {
HeadacheTheme {
Surface {
ReportBox(
item = preview,
)
}
}
}
object ReportBoxPreviewHelper {
fun month(
month: Int,
headache: Int = 0,
pills: Int = 0,
) = ReportBoxUio.Month(
date = Calendar.getInstance().apply {
set(Calendar.MONTH, month)
}.time,
stats = listOf(
ReportBoxUio.Bar(
color = HeadacheColorPalette.Calendar.Headache,
label = "$headache",
ratio = (headache.toFloat() / 30).coerceIn(0f, 1f),
),
ReportBoxUio.Bar(
color = HeadacheColorPalette.Calendar.Pill,
label = "$pills",
ratio = (pills.toFloat() / 24).coerceIn(0f, 1f),
),
).sortedByDescending { it.ratio },
)
}
private class ReportBoxPreviewProvider : PreviewParameterProvider<ReportBoxUio> {
override val values: Sequence<ReportBoxUio>
get() = with(ReportBoxPreviewHelper) {
sequenceOf(
ReportBoxUio(
year = 2025,
months = listOf(
month(month = Calendar.JANUARY, headache = 6, pills = 10),
month(month = Calendar.FEBRUARY, headache = 15, pills = 24),
month(month = Calendar.MARCH, headache = 14, pills = 16),
month(month = Calendar.APRIL, headache = 16, pills = 18),
month(month = Calendar.MARCH, headache = 14, pills = 20),
month(month = Calendar.JUNE, headache = 12, pills = 13),
month(month = Calendar.JULY, headache = 7, pills = 3),
month(month = Calendar.AUGUST, headache = 8, pills = 5),
month(month = Calendar.SEPTEMBER, headache = 8, pills = 5),
month(month = Calendar.OCTOBER),
month(month = Calendar.NOVEMBER),
month(month = Calendar.DECEMBER),
)
)
)
}
}

View file

@ -0,0 +1,59 @@
package com.pixelized.headache.ui.page.summary.report
import android.icu.util.Calendar
import com.pixelized.headache.repository.event.Event
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
import com.pixelized.headache.utils.extention.event
import javax.inject.Inject
class ReportFactory @Inject constructor() {
fun convertToUio(
maxPillAmountPerMonth: Int,
events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
): List<ReportBoxUio> {
return events
.map { yearEntry ->
ReportBoxUio(
year = yearEntry.key,
months = yearEntry.value.map { monthEntry ->
var headache = 0
var pills = 0
monthEntry.value.values.forEach { events ->
headache += events.size
pills += events.sumOf { it.pills.size }
}
val dayInMonth = Calendar.getInstance().let {
it.set(Calendar.YEAR, yearEntry.key)
it.set(Calendar.MONTH, monthEntry.key + 1)
it.set(Calendar.DAY_OF_MONTH, 1)
it.add(Calendar.DAY_OF_YEAR, -1)
it.get(Calendar.DAY_OF_MONTH)
}
ReportBoxUio.Month(
date = Calendar.getInstance().apply {
event = Event.Date(
day = 1,
month = monthEntry.key,
year = yearEntry.key
)
}.time,
stats = listOf(
ReportBoxUio.Bar(
color = HeadacheColorPalette.Calendar.Headache,
label = "$headache",
ratio = headache.toFloat() / dayInMonth.toFloat(),
),
ReportBoxUio.Bar(
color = HeadacheColorPalette.Calendar.Pill,
label = "$pills",
ratio = pills.toFloat() / maxPillAmountPerMonth.toFloat(),
),
).sortedByDescending { it.ratio },
)
}
)
}
.sortedByDescending { it.year }
}
}

View file

@ -0,0 +1,237 @@
package com.pixelized.headache.ui.page.summary.report
import android.icu.util.Calendar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.headache.R
import com.pixelized.headache.ui.theme.HeadacheTheme
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
import com.pixelized.headache.utils.extention.calculate
@Stable
data object ReportPageDefault {
@Stable
val paddingValues = PaddingValues(
start = 16.dp,
end = 16.dp,
bottom = 16.dp + 16.dp + 56.dp,
)
@Stable
val contentSpace: Dp = 8.dp
@Stable
val barSpace: Dp = ReportBoxDefault.barSpace
}
@Composable
fun ReportPage(
viewModel: ReportViewModel = hiltViewModel(),
) {
val events = viewModel.events.collectAsStateWithLifecycle()
ReportContent(
modifier = Modifier
.keepScreenOn()
.fillMaxSize(),
events = events,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReportContent(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = ReportPageDefault.paddingValues,
contentSpace: Dp = ReportPageDefault.contentSpace,
barSpace: Dp = ReportPageDefault.barSpace,
barSize: DpSize = rememberBarSize(column = 12, paddingValues = paddingValues, space = barSpace),
events: State<List<ReportBoxUio>>,
) {
Scaffold(
modifier = modifier,
contentWindowInsets = remember { WindowInsets(0, 0, 0, 0) },
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = NavigationBarDefaults.containerColor,
),
title = {
Text(text = stringResource(R.string.year_summary_title))
},
actions = {
Column(
modifier = Modifier.padding(end = 16.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = HeadacheColorPalette.Calendar.Headache,
)
)
Text(
style = MaterialTheme.typography.labelSmall,
text = "Jours de migraine",
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(8.dp)
.background(
color = HeadacheColorPalette.Calendar.Pill,
)
)
Text(
style = MaterialTheme.typography.labelSmall,
text = "Prise de cachet",
)
}
}
}
)
},
content = { it ->
LazyColumn(
modifier = Modifier.padding(paddingValues = it),
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(space = contentSpace),
) {
items(
items = events.value
) { item ->
ReportBox(
barSize = barSize,
barSpace = barSpace,
item = item,
)
}
}
}
)
}
@Composable
private fun rememberBarSize(
column: Int,
paddingValues: PaddingValues,
space: Dp,
): DpSize {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val screenWidth = remember(density, windowInfo) {
with(density) { windowInfo.containerSize.width.toDp() }
}
val (start, _, end, _) = paddingValues.calculate()
return remember {
DpSize(
width = (screenWidth - space * (column - 1) - start - end) / column,
height = ReportBoxDefault.barSize.height,
)
}
}
@Composable
@Preview
private fun ReportPreview(
@PreviewParameter(ReportPreviewProvider::class) preview: List<ReportBoxUio>,
) {
HeadacheTheme {
Surface {
ReportContent(
events = remember { mutableStateOf(preview) },
)
}
}
}
private class ReportPreviewProvider : PreviewParameterProvider<List<ReportBoxUio>> {
override val values: Sequence<List<ReportBoxUio>>
get() = with(ReportBoxPreviewHelper) {
sequenceOf(
listOf(
ReportBoxUio(
year = 2025,
months = listOf(
month(month = Calendar.JANUARY, headache = 6, pills = 10),
month(month = Calendar.FEBRUARY, headache = 15, pills = 24),
month(month = Calendar.MARCH, headache = 14, pills = 16),
month(month = Calendar.APRIL, headache = 16, pills = 18),
month(month = Calendar.MARCH, headache = 14, pills = 20),
month(month = Calendar.JUNE, headache = 12, pills = 13),
month(month = Calendar.JULY, headache = 7, pills = 3),
month(month = Calendar.AUGUST, headache = 8, pills = 5),
month(month = Calendar.SEPTEMBER, headache = 8, pills = 5),
month(month = Calendar.OCTOBER, headache = 0, pills = 0),
month(month = Calendar.NOVEMBER, headache = 0, pills = 0),
month(month = Calendar.DECEMBER, headache = 0, pills = 0),
),
),
ReportBoxUio(
year = 2024,
months = listOf(
month(month = Calendar.JANUARY, headache = 14),
month(month = Calendar.FEBRUARY, headache = 15),
month(month = Calendar.MARCH, headache = 15),
month(month = Calendar.APRIL, headache = 10),
month(month = Calendar.MARCH, headache = 7),
month(month = Calendar.JUNE, headache = 15),
month(month = Calendar.JULY, headache = 7),
month(month = Calendar.AUGUST, headache = 11, pills = 12),
month(month = Calendar.SEPTEMBER, headache = 12, pills = 15),
month(month = Calendar.OCTOBER, headache = 5, pills = 8),
month(month = Calendar.NOVEMBER, headache = 17, pills = 22),
month(month = Calendar.DECEMBER, headache = 12, pills = 17),
),
)
)
)
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.headache.ui.page.summary.report
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.headache.repository.event.EventRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class ReportViewModel @Inject constructor(
eventRepository: EventRepository,
reportFactory: ReportFactory,
) : ViewModel() {
val events: StateFlow<List<ReportBoxUio>> = combine(
eventRepository.maxPillAmountPerMonthFlow(),
eventRepository.eventsMapFlow(),
reportFactory::convertToUio
).stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = emptyList(),
)
}

View file

@ -8,7 +8,7 @@ import javax.inject.Inject
class YearSummaryFactory @Inject constructor() { class YearSummaryFactory @Inject constructor() {
fun convertToUio( fun convertToUio(
events: Map<Int, Map<Int, Map<Int, Event>>>, events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
): List<YearUio> { ): List<YearUio> {
val monthFirstDayCalendar = Calendar.getInstance().apply { val monthFirstDayCalendar = Calendar.getInstance().apply {
firstDayOfWeek = Calendar.MONDAY firstDayOfWeek = Calendar.MONDAY
@ -49,7 +49,7 @@ class YearSummaryFactory @Inject constructor() {
} }
val weeks = (1..monthLastDayCalendar.get(Calendar.DAY_OF_MONTH)) val weeks = (1..monthLastDayCalendar.get(Calendar.DAY_OF_MONTH))
.fold(initial = initial) { accumulator, dayNumber -> .fold(initial = initial) { accumulator, dayNumber ->
val event: Event? = monthEntry.value.get(dayNumber) val event: List<Event> = monthEntry.value[dayNumber] ?: emptyList()
val weekIndex = currentDayCalendar val weekIndex = currentDayCalendar
.apply { .apply {
set(Calendar.YEAR, yearEntry.key) set(Calendar.YEAR, yearEntry.key)
@ -60,8 +60,8 @@ class YearSummaryFactory @Inject constructor() {
.get(Calendar.WEEK_OF_MONTH) - 1 .get(Calendar.WEEK_OF_MONTH) - 1
val day = DayUio( val day = DayUio(
number = dayNumber, number = dayNumber,
headache = event != null, headache = event.isNotEmpty(),
pills = event?.pills?.map { it.color } ?: emptyList(), pills = event.flatMap { it.pills.map { pill -> pill.color } },
) )
accumulator.also { acc -> accumulator.also { acc ->
acc[weekIndex] = acc.get(index = weekIndex).also { week -> acc[weekIndex] = acc.get(index = weekIndex).also { week ->

View file

@ -142,7 +142,7 @@ fun YearSummaryMonth(
width = localCellSize.width - 0f, width = localCellSize.width - 0f,
height = localCellSize.height - (pillSize.height + space) * 2, height = localCellSize.height - (pillSize.height + space) * 2,
), ),
color = HeadacheColorPalette.Pill.Unknown, color = HeadacheColorPalette.Calendar.Headache,
) )
} }

View file

@ -83,23 +83,6 @@ fun YearSummaryPage(
) )
} }
@Composable
private fun rememberDaySize(
column: Int,
paddingValues: PaddingValues = YearSummaryPageDefault.paddingValues,
space: Dp = YearSummaryPageDefault.space,
): Dp {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val screenWidth = remember(density, windowInfo) {
with(density) { windowInfo.containerSize.width.toDp() }
}
val (start, _, end, _) = paddingValues.calculate()
return remember {
(screenWidth - space * (column - 1) - start - end) / (7 * column)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun YearSummaryContent( fun YearSummaryContent(
@ -169,6 +152,23 @@ fun YearSummaryContent(
) )
} }
@Composable
private fun rememberDaySize(
column: Int,
paddingValues: PaddingValues = YearSummaryPageDefault.paddingValues,
space: Dp = YearSummaryPageDefault.space,
): Dp {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val screenWidth = remember(density, windowInfo) {
with(density) { windowInfo.containerSize.width.toDp() }
}
val (start, _, end, _) = paddingValues.calculate()
return remember {
(screenWidth - space * (column - 1) - start - end) / (7 * column)
}
}
@Composable @Composable
@Preview() @Preview()
private fun YearSummaryPreview( private fun YearSummaryPreview(

View file

@ -3,14 +3,6 @@ package com.pixelized.headache.ui.theme.color
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import javax.annotation.concurrent.Immutable import javax.annotation.concurrent.Immutable
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@Immutable @Immutable
object HeadacheColorPalette { object HeadacheColorPalette {
@ -23,6 +15,12 @@ object HeadacheColorPalette {
val Eletriptan40 = Additional.VeryLightPink val Eletriptan40 = Additional.VeryLightPink
} }
@Immutable
object Calendar {
val Headache = Additional.LightRed
val Pill = Additional.DarkRed
}
@Immutable @Immutable
object Additional { object Additional {
val VeryDarkBlue: Color = Color(0xFF09179D) val VeryDarkBlue: Color = Color(0xFF09179D)

View file

@ -40,7 +40,7 @@ data class HeadacheColors(
fun headacheDarkColorScheme( fun headacheDarkColorScheme(
base: ColorScheme = darkColorScheme(), base: ColorScheme = darkColorScheme(),
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar( calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
headache = HeadacheColorPalette.Additional.Red, headache = HeadacheColorPalette.Calendar.Headache,
onHeadache = Color.White, onHeadache = Color.White,
), ),
pill: HeadacheColors.Pill = HeadacheColors.Pill( pill: HeadacheColors.Pill = HeadacheColors.Pill(
@ -61,7 +61,7 @@ fun headacheDarkColorScheme(
fun headacheLightColorScheme( fun headacheLightColorScheme(
base: ColorScheme = lightColorScheme(), base: ColorScheme = lightColorScheme(),
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar( calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
headache = HeadacheColorPalette.Additional.Red, headache = HeadacheColorPalette.Calendar.Headache,
onHeadache = Color.White, onHeadache = Color.White,
), ),
pill: HeadacheColors.Pill = HeadacheColors.Pill( pill: HeadacheColors.Pill = HeadacheColors.Pill(
@ -94,7 +94,7 @@ fun calculateElevatedColor(
@ReadOnlyComposable @ReadOnlyComposable
@Composable @Composable
private fun calculateForegroundColor(color: Color, elevation: Dp): Color { fun calculateForegroundColor(color: Color, elevation: Dp): Color {
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
return color.copy(alpha = alpha) return color.copy(alpha = alpha)
} }