Coverage Summary for Class: TransactionDetailsHeroCardKt (com.georgeci.moneysurfer.feature.transaction.details)

Class Class, % Method, % Branch, % Line, % Instruction, %
TransactionDetailsHeroCardKt 100% (1/1) 100% (4/4) 71.9% (23/32) 99% (95/96) 97.7% (819/838)


 package com.georgeci.moneysurfer.feature.transaction.details
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import com.georgeci.moneysurfer.domain.primitives.TransactionType
 import com.georgeci.moneysurfer.uikit.components.SurferCategoryBubble
 import com.georgeci.moneysurfer.uikit.components.SurferCategoryPalette
 import com.georgeci.moneysurfer.uikit.components.SurferCategoryVisual
 import com.georgeci.moneysurfer.uikit.components.base.SurferSplitAmount
 import com.georgeci.moneysurfer.uikit.components.base.SurferSplitAmountTier
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import moneysurfer.feature.transaction.generated.resources.Res
 import moneysurfer.feature.transaction.generated.resources.transaction_details_status_planned
 import moneysurfer.feature.transaction.generated.resources.transaction_details_status_posted
 import moneysurfer.feature.transaction.generated.resources.transaction_details_type_expense
 import moneysurfer.feature.transaction.generated.resources.transaction_details_type_income
 import moneysurfer.feature.transaction.generated.resources.transaction_details_type_transfer
 import org.jetbrains.compose.resources.stringResource
 
 /** The hero band's gradient wash, over the category tint. */
 private const val HERO_GRADIENT_ALPHA = 0.22f
 
 /**
  * Hero bubble and hue per variant: a transfer is neutral with a swap glyph (money moved sideways,
  * its category is the seeded system one), income is green whatever the category says, and an
  * expense keeps its category's own colour.
  *
  * The category's stored appearance is read rather than its *name* hashed — two categories renamed
  * to the same word used to share a colour, and a rename silently repainted the screen.
  */
 @Composable
 internal fun heroVisualFor(state: TransactionDetailsState.Content): SurferCategoryVisual {
     val categoryVisual = SurferCategoryPalette.visualFor(
         id = state.categoryId,
         iconKey = state.categoryIconKey,
         hue = state.categoryHue,
         systemKind = state.categorySystemKind,
     )
     return when {
         state.isTransfer -> SurferCategoryVisual(
             icon = SurferCategoryPalette.TransferIcon,
             tint = SurferCategoryPalette.TransferTint,
         )
         state.isIncome -> categoryVisual.copy(tint = AppTheme.semanticColors.income)
         else -> categoryVisual
     }
 }
 
 /** `TRANSFER · POSTED`, or `GROCERIES · EXPENSE · POSTED` once there is a category to name. */
 @Composable
 internal fun heroHeaderFor(state: TransactionDetailsState.Content): String {
     val typeLabel = stringResource(
         when {
             state.isTransfer -> Res.string.transaction_details_type_transfer
             state.type == TransactionType.INCOME -> Res.string.transaction_details_type_income
             else -> Res.string.transaction_details_type_expense
         },
     ).uppercase()
     val statusLabel = stringResource(
         if (state.isPlanned) {
             Res.string.transaction_details_status_planned
         } else {
             Res.string.transaction_details_status_posted
         },
     ).uppercase()
     // A transfer's category is always the seeded "Transfer" one — naming it would just repeat
     // the type label.
     return if (state.isTransfer || state.categoryName.isBlank()) {
         "$typeLabel · $statusLabel"
     } else {
         "${state.categoryName.uppercase()} · $typeLabel · $statusLabel"
     }
 }
 
 @Composable
 internal fun HeroCard(
     header: String,
     headerColor: Color,
     /** Bubble glyph and hue in one, as [heroVisualFor] resolved them. */
     visual: SurferCategoryVisual,
     formattedAmount: String,
     note: String,
     formattedDate: String,
     isPlanned: Boolean,
 ) {
     val outlineVariant = AppTheme.materialColors.outlineVariant
     val surface = AppTheme.materialColors.surface
     val heroBrush = Brush.linearGradient(
         colors = listOf(
             visual.tint.copy(alpha = HERO_GRADIENT_ALPHA),
             surface,
         ),
     )
     Column(
         modifier = Modifier
             .fillMaxWidth()
             .padding(horizontal = 16.dp)
             .padding(bottom = 16.dp)
             .clip(RoundedCornerShape(28.dp))
             .background(heroBrush)
             .border(1.dp, outlineVariant, RoundedCornerShape(28.dp))
             .padding(horizontal = 20.dp, vertical = 22.dp),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         SurferCategoryBubble(icon = visual.icon, tint = visual.tint, size = 64.dp)
         Spacer(Modifier.height(12.dp))
         Text(
             text = header,
             style = AppTheme.typography.labelLarge,
             color = headerColor,
             textAlign = TextAlign.Center,
         )
         Spacer(Modifier.height(6.dp))
         SurferSplitAmount(
             formattedAmount = formattedAmount,
             tier = SurferSplitAmountTier.Hero,
             color = AppTheme.materialColors.onSurface,
             signAlpha = 0.7f,
             fractionAlpha = 0.55f,
         )
         if (note.isNotBlank()) {
             Spacer(Modifier.height(8.dp))
             Text(
                 text = note,
                 style = AppTheme.typography.titleMedium,
                 color = AppTheme.materialColors.onSurface,
                 textAlign = TextAlign.Center,
             )
         }
         Spacer(Modifier.height(2.dp))
         Text(
             text = formattedDate,
             style = AppTheme.typography.bodySmall,
             color = AppTheme.materialColors.onSurfaceVariant,
         )
         if (isPlanned) {
             Spacer(Modifier.height(10.dp))
             PlannedPill()
         }
     }
 }
 
 @Composable
 private fun PlannedPill() {
     Row(
         modifier = Modifier
             .clip(RoundedCornerShape(percent = 50))
             .background(AppTheme.materialColors.tertiaryContainer)
             .padding(horizontal = 10.dp, vertical = 4.dp),
         verticalAlignment = Alignment.CenterVertically,
         horizontalArrangement = Arrangement.spacedBy(6.dp),
     ) {
         Icon(
             imageVector = SurferIcons.Clock,
             contentDescription = SurferSemantics.Decorative,
             tint = AppTheme.materialColors.onTertiaryContainer,
             modifier = Modifier.size(12.dp),
         )
         Text(
             text = stringResource(Res.string.transaction_details_status_planned),
             style = AppTheme.typography.labelSmall,
             color = AppTheme.materialColors.onTertiaryContainer,
         )
     }
 }