Coverage Summary for Class: DashboardSpentMonthWidgetKt (com.georgeci.moneysurfer.feature.dashboard)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| DashboardSpentMonthWidgetKt |
100%
(6/6)
|
84.4%
(27/32)
|
94.4%
(34/36)
|
96.8%
(477/493)
|
| DashboardSpentMonthWidgetKt$WhenMappings |
|
| Total |
100%
(6/6)
|
84.4%
(27/32)
|
94.4%
(34/36)
|
96.8%
(477/493)
|
package com.georgeci.moneysurfer.feature.dashboard
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.domain.insight.SpendTrend
import com.georgeci.moneysurfer.uikit.theme.AppTheme
import com.georgeci.moneysurfer.uikit.widgets.LocalSurferWidgetSize
import com.georgeci.moneysurfer.uikit.widgets.SurferSpentMonthWidget
import com.georgeci.moneysurfer.uikit.widgets.SurferWidgetSize
import moneysurfer.feature.dashboard.generated.resources.Res
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_delta_down
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_delta_flat
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_delta_up
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_no_budget
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_of_budget
import moneysurfer.feature.dashboard.generated.resources.dashboard_spent_month_title
import org.jetbrains.compose.resources.stringResource
/**
* What this month has cost, how far that is through the budget, and how it compares to last month.
*
* Like the burn rate and safe-to-spend cards this one keeps drawing with no budget behind it — the
* amount is arithmetic over transactions, and "no budget set" is a state the card is partly *for*.
* Only the bar needs a cap, and it is simply left empty without one rather than filled against a
* limit nobody set.
*
* A dash stands in for the amount only while [DashboardState.Content.spentMonth] itself is null,
* which is the frame before a workspace and its base currency resolve — never a month that merely
* booked nothing, which is a real zero and prints as one.
*/
@Composable
internal fun SpentMonthWidget(state: DashboardState.Content) {
val spentMonth = state.spentMonth
SurferSpentMonthWidget(
title = stringResource(Res.string.dashboard_spent_month_title),
spent = spentMonth?.spentFormatted ?: NO_AMOUNT,
progress = spentMonth?.progress ?: 0f,
caption = spentMonth.caption(),
trailingLabel = spentMonth?.delta?.label(),
trailingLabelColor = spentMonth?.delta?.color(),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = 8.dp)
.height(spentMonthCardHeight())
.testTag(DashboardTestTags.SpentMonth),
)
}
/**
* The line under the amount: the cap it is measured against, or the fact that nothing caps it.
*
* Null while the figure itself is unresolved, which is *not* the same as no budget — a workspace the
* device has not pulled yet may well have one, and saying otherwise states something about the
* user's money the app has not read. The card loses nothing by staying quiet: its height is pinned
* below, and `SurferSpentMonthWidget` drops the caption row entirely when it has nothing for it.
*/
@Composable
private fun SpentMonthUi?.caption(): String? {
val resolved = this ?: return null
val cap = resolved.capFormatted ?: return stringResource(Res.string.dashboard_spent_month_no_budget)
return stringResource(Res.string.dashboard_spent_month_of_budget, cap)
}
/**
* The delta as a sentence. The direction picks the wording rather than a sign being formatted into
* the percentage, so a translation can put the arrow wherever its language wants it — and a flat
* month gets a sentence of its own rather than "+0%".
*/
@Composable
private fun SpentMonthDeltaUi.label(): String = when (trend) {
SpendTrend.Up -> stringResource(Res.string.dashboard_spent_month_delta_up, percent)
SpendTrend.Down -> stringResource(Res.string.dashboard_spent_month_delta_down, percent)
SpendTrend.Flat -> stringResource(Res.string.dashboard_spent_month_delta_flat)
}
/**
* `uikit` does not depend on `domain`, so the direction is resolved to a colour here.
*
* Spending more is the error colour and spending less is the income green — the same reading
* [SpendTrend.tone] makes in the insights engine, and the pair the rest of the app already uses for
* money going the wrong and the right way. A flat month takes the widget's default, because there is
* no direction to colour.
*/
@Composable
private fun SpentMonthDeltaUi.color(): Color? = when (trend) {
SpendTrend.Up -> AppTheme.materialColors.error
SpendTrend.Down -> AppTheme.semanticColors.income
SpendTrend.Flat -> null
}
/**
* The height this card is drawn at, for the density in scope.
*
* Fixed, unlike the list-shaped cards' floor: it draws the same four rows whatever the month holds,
* so it needs a height rather than a minimum — `SurferSpentMonthWidget` fills what it is given. One
* value per density, so the size the user picked changes the card's footprint and not only its
* typography.
*
* Internal and shared with the customize picker's tile rather than restated there: the picker exists
* to show what the two sizes look like, so a tile drawn at a height the dashboard does not use is a
* thumbnail that misrepresents the choice it is offering.
*/
@Composable
internal fun spentMonthCardHeight(): Dp =
if (LocalSurferWidgetSize.current == SurferWidgetSize.Compact) {
SPENT_MONTH_COMPACT_HEIGHT
} else {
SPENT_MONTH_HEIGHT
}
private val SPENT_MONTH_HEIGHT = 160.dp
private val SPENT_MONTH_COMPACT_HEIGHT = 132.dp
/** Stands in for the amount until a workspace and a base currency resolve — see [SpentMonthWidget]. */
private const val NO_AMOUNT = "—"