mirror of
https://github.com/andwati/ledga.git
synced 2026-10-18 03:12:55 +00:00
v1.4.7: People view (sent/received) + surface Large filter
- New 'People' segment on Activity: browse who you've sent money to or received from, aggregated by name with totals and counts. Search by name, filter by a minimum total with a slider, toggle Sent/Received, and tap a person to see every transaction with them. - Moved the 'Large' filter chip to the front of the row so it's visible without scrolling (it was the last, off-screen chip).
This commit is contained in:
@@ -27,8 +27,8 @@ android {
|
|||||||
applicationId = "com.ledga.app"
|
applicationId = "com.ledga.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 16
|
versionCode = 17
|
||||||
versionName = "1.4.6"
|
versionName = "1.4.7"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,45 @@ interface TransactionDao {
|
|||||||
@Query("SELECT * FROM transactions WHERE transactionCode = :code LIMIT 1")
|
@Query("SELECT * FROM transactions WHERE transactionCode = :code LIMIT 1")
|
||||||
suspend fun getTransactionByCode(code: String): TransactionEntity?
|
suspend fun getTransactionByCode(code: String): TransactionEntity?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* People you've transacted with, aggregated by name: total + count.
|
||||||
|
* [types] selects the direction — SEND/MPESA_GLOBAL for "sent to",
|
||||||
|
* RECEIVED for "received from". Filtered by a name fragment and a minimum
|
||||||
|
* total. Powers the People view.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT recipientName as recipientName, SUM(amount) as totalAmount, COUNT(*) as transactionCount
|
||||||
|
FROM transactions
|
||||||
|
WHERE type IN (:types)
|
||||||
|
AND recipientName IS NOT NULL AND recipientName != ''
|
||||||
|
AND recipientName LIKE '%' || :query || '%' COLLATE NOCASE
|
||||||
|
AND (:accountId IS NULL OR accountId = :accountId)
|
||||||
|
GROUP BY recipientName
|
||||||
|
HAVING SUM(amount) >= :minTotal
|
||||||
|
ORDER BY totalAmount DESC
|
||||||
|
""")
|
||||||
|
fun getPeopleByTypes(
|
||||||
|
types: List<TransactionType>,
|
||||||
|
query: String,
|
||||||
|
minTotal: Double,
|
||||||
|
accountId: Long? = null,
|
||||||
|
): Flow<List<TopMerchant>>
|
||||||
|
|
||||||
|
/** A single person's transactions of the given types, newest first. */
|
||||||
|
@Transaction
|
||||||
|
@Query("""
|
||||||
|
SELECT * FROM transactions
|
||||||
|
WHERE type IN (:types)
|
||||||
|
AND recipientName = :name
|
||||||
|
AND (:accountId IS NULL OR accountId = :accountId)
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
""")
|
||||||
|
fun getTransactionsForRecipient(
|
||||||
|
name: String,
|
||||||
|
types: List<TransactionType>,
|
||||||
|
accountId: Long? = null,
|
||||||
|
): Flow<List<TransactionWithCategory>>
|
||||||
|
|
||||||
/** Transactions at or above [threshold] — powers the "Large" filter. */
|
/** Transactions at or above [threshold] — powers the "Large" filter. */
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("""
|
@Query("""
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ class TransactionRepository @Inject constructor(
|
|||||||
fun getLargeTransactions(threshold: Double): Flow<List<TransactionWithCategory>> =
|
fun getLargeTransactions(threshold: Double): Flow<List<TransactionWithCategory>> =
|
||||||
selectedAccount.flatMapLatest { transactionDao.getLargeTransactions(threshold, it) }
|
selectedAccount.flatMapLatest { transactionDao.getLargeTransactions(threshold, it) }
|
||||||
|
|
||||||
|
fun getPeopleByTypes(types: List<TransactionType>, query: String, minTotal: Double): Flow<List<TopMerchant>> =
|
||||||
|
selectedAccount.flatMapLatest { transactionDao.getPeopleByTypes(types, query, minTotal, it) }
|
||||||
|
|
||||||
|
fun getTransactionsForRecipient(name: String, types: List<TransactionType>): Flow<List<TransactionWithCategory>> =
|
||||||
|
selectedAccount.flatMapLatest { transactionDao.getTransactionsForRecipient(name, types, it) }
|
||||||
|
|
||||||
fun getDailySpending(startTime: Long, endTime: Long): Flow<List<DailySpending>> =
|
fun getDailySpending(startTime: Long, endTime: Long): Flow<List<DailySpending>> =
|
||||||
selectedAccount.flatMapLatest { transactionDao.getDailySpending(startTime, endTime, it) }
|
selectedAccount.flatMapLatest { transactionDao.getDailySpending(startTime, endTime, it) }
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ import androidx.compose.foundation.clickable
|
|||||||
import com.ledga.app.ui.components.v2.LedgaTopBar
|
import com.ledga.app.ui.components.v2.LedgaTopBar
|
||||||
import com.ledga.app.ui.theme.LedgaText
|
import com.ledga.app.ui.theme.LedgaText
|
||||||
import com.ledga.app.ui.theme.Space
|
import com.ledga.app.ui.theme.Space
|
||||||
|
import com.ledga.app.ui.people.PeopleScreen
|
||||||
import com.ledga.app.ui.transactions.TransactionsScreen
|
import com.ledga.app.ui.transactions.TransactionsScreen
|
||||||
import com.ledga.app.ui.trends.TrendsScreen
|
import com.ledga.app.ui.trends.TrendsScreen
|
||||||
|
|
||||||
private enum class Segment { Transactions, Trends }
|
private enum class Segment { Transactions, People, Trends }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity tab — pairs the Transactions list and Trends analytics behind
|
* Activity tab — pairs the Transactions list and Trends analytics behind
|
||||||
@@ -51,6 +52,7 @@ fun ActivityScreen() {
|
|||||||
|
|
||||||
when (segment) {
|
when (segment) {
|
||||||
Segment.Transactions -> TransactionsScreen()
|
Segment.Transactions -> TransactionsScreen()
|
||||||
|
Segment.People -> PeopleScreen()
|
||||||
Segment.Trends -> TrendsScreen()
|
Segment.Trends -> TrendsScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package com.ledga.app.ui.people
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.ledga.app.data.db.dao.TopMerchant
|
||||||
|
import com.ledga.app.data.parser.TransactionDirection
|
||||||
|
import com.ledga.app.ui.components.parseColor
|
||||||
|
import com.ledga.app.ui.components.v2.Avatar
|
||||||
|
import com.ledga.app.ui.components.v2.BentoCard
|
||||||
|
import com.ledga.app.ui.components.v2.LedgaChip
|
||||||
|
import com.ledga.app.ui.components.v2.LedgaSearchField
|
||||||
|
import com.ledga.app.ui.components.v2.TransactionRowV2
|
||||||
|
import com.ledga.app.ui.theme.LedgaAccent
|
||||||
|
import com.ledga.app.ui.theme.LedgaInflow
|
||||||
|
import com.ledga.app.ui.theme.LedgaText
|
||||||
|
import com.ledga.app.ui.theme.Space
|
||||||
|
import com.ledga.app.util.CurrencyFormatter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PeopleScreen(viewModel: PeopleViewModel = hiltViewModel()) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
if (state.selectedPerson != null) {
|
||||||
|
PersonSheet(
|
||||||
|
name = state.selectedPerson!!,
|
||||||
|
modeLabel = state.mode.label,
|
||||||
|
transactions = state.selectedPersonTransactions,
|
||||||
|
onDismiss = { viewModel.clearSelection() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = Space.Screen),
|
||||||
|
) {
|
||||||
|
// Sent / Received toggle
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(Space.s3)) {
|
||||||
|
PeopleMode.entries.forEach { mode ->
|
||||||
|
LedgaChip(
|
||||||
|
label = mode.label,
|
||||||
|
selected = state.mode == mode,
|
||||||
|
onClick = { viewModel.setMode(mode) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Space.s4))
|
||||||
|
|
||||||
|
LedgaSearchField(
|
||||||
|
value = state.query,
|
||||||
|
onValueChange = { viewModel.setQuery(it) },
|
||||||
|
placeholder = "Search people by name…",
|
||||||
|
)
|
||||||
|
|
||||||
|
MinTotalSlider(
|
||||||
|
minTotal = state.minTotal,
|
||||||
|
onChange = { viewModel.setMinTotal(it) },
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Space.s4))
|
||||||
|
|
||||||
|
if (state.people.isEmpty()) {
|
||||||
|
BentoCard {
|
||||||
|
Text(
|
||||||
|
text = if (state.query.isNotBlank() || state.minTotal > 0)
|
||||||
|
"No people match these filters."
|
||||||
|
else "No ${state.mode.label.lowercase()} people yet.",
|
||||||
|
style = LedgaText.BodyM,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Space.s4),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 140.dp),
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "${state.people.size} ${if (state.people.size == 1) "person" else "people"}",
|
||||||
|
style = LedgaText.Caption,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(state.people, key = { it.recipientName }) { person ->
|
||||||
|
PersonRow(
|
||||||
|
person = person,
|
||||||
|
isInflow = state.mode == PeopleMode.RECEIVED,
|
||||||
|
onClick = { viewModel.selectPerson(person.recipientName) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PersonRow(person: TopMerchant, isInflow: Boolean, onClick: () -> Unit) {
|
||||||
|
BentoCard(onClick = onClick) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Avatar(
|
||||||
|
initials = initialsOf(person.recipientName),
|
||||||
|
color = LedgaAccent,
|
||||||
|
size = 40.dp,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = Space.s4),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = person.recipientName,
|
||||||
|
style = LedgaText.BodyL,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${person.transactionCount} ${if (person.transactionCount == 1) "transaction" else "transactions"}",
|
||||||
|
style = LedgaText.Caption,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = CurrencyFormatter.formatKshCompact(person.totalAmount),
|
||||||
|
style = LedgaText.TitleS,
|
||||||
|
color = if (isInflow) LedgaInflow else MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MinTotalSlider(minTotal: Double, onChange: (Double) -> Unit) {
|
||||||
|
var value by remember(minTotal) { mutableStateOf(minTotal.toFloat()) }
|
||||||
|
Column(modifier = Modifier.padding(top = Space.s4)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "Total at least",
|
||||||
|
style = LedgaText.Caption,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (value <= 0f) "Any amount"
|
||||||
|
else CurrencyFormatter.formatKshCompact(value.toDouble()),
|
||||||
|
style = LedgaText.TitleS,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { value = it },
|
||||||
|
onValueChangeFinished = { onChange(value.toDouble()) },
|
||||||
|
valueRange = 0f..100_000f,
|
||||||
|
steps = 99, // 1K increments
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = LedgaAccent,
|
||||||
|
activeTrackColor = LedgaAccent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PersonSheet(
|
||||||
|
name: String,
|
||||||
|
modeLabel: String,
|
||||||
|
transactions: List<com.ledga.app.data.db.entity.TransactionWithCategory>,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val total = transactions.sumOf { it.transaction.amount }
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
.padding(bottom = 24.dp),
|
||||||
|
) {
|
||||||
|
Text(text = name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Text(
|
||||||
|
text = "${modeLabel.lowercase()} · ${CurrencyFormatter.formatKsh(total)} over ${transactions.size} " +
|
||||||
|
if (transactions.size == 1) "transaction" else "transactions",
|
||||||
|
style = LedgaText.BodyM,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
HorizontalDivider()
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
items(transactions, key = { it.transaction.id }) { twc ->
|
||||||
|
TransactionRowV2(
|
||||||
|
recipient = twc.transaction.recipientName ?: name,
|
||||||
|
amount = twc.transaction.amount,
|
||||||
|
isInflow = twc.transaction.direction == TransactionDirection.INFLOW,
|
||||||
|
category = twc.category?.name,
|
||||||
|
color = twc.category?.color?.let(::parseColor)
|
||||||
|
?: androidx.compose.ui.graphics.Color.Gray,
|
||||||
|
icon = twc.category?.icon ?: "person",
|
||||||
|
balance = twc.transaction.balance,
|
||||||
|
timestamp = twc.transaction.timestamp,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialsOf(name: String): String =
|
||||||
|
name.trim().split(Regex("\\s+"))
|
||||||
|
.mapNotNull { it.firstOrNull()?.toString() }
|
||||||
|
.take(2)
|
||||||
|
.joinToString("")
|
||||||
|
.uppercase()
|
||||||
|
.ifEmpty { "?" }
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.ledga.app.ui.people
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.ledga.app.data.db.dao.TopMerchant
|
||||||
|
import com.ledga.app.data.db.entity.TransactionWithCategory
|
||||||
|
import com.ledga.app.data.parser.TransactionType
|
||||||
|
import com.ledga.app.data.repository.TransactionRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/** Direction of money flow for the People view. */
|
||||||
|
enum class PeopleMode(val label: String, val types: List<TransactionType>) {
|
||||||
|
SENT("Sent to", listOf(TransactionType.SEND, TransactionType.MPESA_GLOBAL)),
|
||||||
|
RECEIVED("Received from", listOf(TransactionType.RECEIVED)),
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PeopleUiState(
|
||||||
|
val mode: PeopleMode = PeopleMode.SENT,
|
||||||
|
val query: String = "",
|
||||||
|
val minTotal: Double = 0.0,
|
||||||
|
val people: List<TopMerchant> = emptyList(),
|
||||||
|
/** When non-null, the drill-down sheet shows this person's transactions. */
|
||||||
|
val selectedPerson: String? = null,
|
||||||
|
val selectedPersonTransactions: List<TransactionWithCategory> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
|
@HiltViewModel
|
||||||
|
class PeopleViewModel @Inject constructor(
|
||||||
|
private val transactionRepository: TransactionRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _mode = MutableStateFlow(PeopleMode.SENT)
|
||||||
|
private val _query = MutableStateFlow("")
|
||||||
|
private val _minTotal = MutableStateFlow(0.0)
|
||||||
|
private val _selectedPerson = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
|
private val people = combine(
|
||||||
|
_mode,
|
||||||
|
_query.debounce(250),
|
||||||
|
_minTotal,
|
||||||
|
) { mode, q, min -> Triple(mode, q, min) }
|
||||||
|
.flatMapLatest { (mode, q, min) ->
|
||||||
|
transactionRepository.getPeopleByTypes(mode.types, q, min)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val selectedTransactions = combine(_selectedPerson, _mode) { name, mode -> name to mode }
|
||||||
|
.flatMapLatest { (name, mode) ->
|
||||||
|
if (name == null) flowOf(emptyList())
|
||||||
|
else transactionRepository.getTransactionsForRecipient(name, mode.types)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uiState: StateFlow<PeopleUiState> = combine(
|
||||||
|
combine(_mode, _query, _minTotal) { mode, q, min -> Triple(mode, q, min) },
|
||||||
|
people,
|
||||||
|
_selectedPerson,
|
||||||
|
selectedTransactions,
|
||||||
|
) { (mode, query, minTotal), people, selected, txns ->
|
||||||
|
PeopleUiState(
|
||||||
|
mode = mode,
|
||||||
|
query = query,
|
||||||
|
minTotal = minTotal,
|
||||||
|
people = people,
|
||||||
|
selectedPerson = selected,
|
||||||
|
selectedPersonTransactions = txns,
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PeopleUiState())
|
||||||
|
|
||||||
|
fun setMode(mode: PeopleMode) { _mode.value = mode }
|
||||||
|
fun setQuery(value: String) { _query.value = value }
|
||||||
|
fun setMinTotal(value: Double) { _minTotal.value = value }
|
||||||
|
fun selectPerson(name: String) { _selectedPerson.value = name }
|
||||||
|
fun clearSelection() { _selectedPerson.value = null }
|
||||||
|
}
|
||||||
@@ -65,14 +65,15 @@ class TransactionsViewModel @Inject constructor(
|
|||||||
|
|
||||||
val FILTERS = mapOf(
|
val FILTERS = mapOf(
|
||||||
"All" to emptyList<TransactionType>(),
|
"All" to emptyList<TransactionType>(),
|
||||||
|
// Amount threshold, not a type — resolved against the user's
|
||||||
|
// large-transaction setting (default Ksh5,000). Kept near the front
|
||||||
|
// so it's visible without scrolling the chip row.
|
||||||
|
LARGE_FILTER to emptyList(),
|
||||||
"Sent" to listOf(TransactionType.SEND, TransactionType.MPESA_GLOBAL),
|
"Sent" to listOf(TransactionType.SEND, TransactionType.MPESA_GLOBAL),
|
||||||
"Received" to listOf(TransactionType.RECEIVED),
|
"Received" to listOf(TransactionType.RECEIVED),
|
||||||
"Bills" to listOf(TransactionType.PAY_BILL),
|
"Bills" to listOf(TransactionType.PAY_BILL),
|
||||||
"Goods" to listOf(TransactionType.BUY_GOODS),
|
"Goods" to listOf(TransactionType.BUY_GOODS),
|
||||||
"Withdraw" to listOf(TransactionType.WITHDRAW_AGENT, TransactionType.WITHDRAW_ATM),
|
"Withdraw" to listOf(TransactionType.WITHDRAW_AGENT, TransactionType.WITHDRAW_ATM),
|
||||||
// Amount threshold, not a type — resolved against the user's
|
|
||||||
// large-transaction setting (default Ksh5,000, configurable).
|
|
||||||
LARGE_FILTER to emptyList(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user