From 3286bcf87a4b0242935cff665306a4ec7beffb8b Mon Sep 17 00:00:00 2001 From: Rafal Wisniewski <2krafal.wisniewski@gmail.com> Date: Fri, 1 May 2026 13:10:27 +0200 Subject: [PATCH] fix: add info text when no data --- .../cc/n0th1ng/tripmoney/data/TripDatabase.kt | 14 +- .../cc/n0th1ng/tripmoney/data/entity/Trip.kt | 4 + .../addexpense/AddExpenseBottomSheet.kt | 44 ++--- .../listexpense/CategorySelectionDialog.kt | 4 +- .../screens/listexpense/ListExpenseScreen.kt | 175 +++++++++++++----- .../screens/statistics/StatisticsScreen.kt | 61 ++++-- .../screens/trippicker/TripPickerScreen.kt | 79 +++++--- app/src/main/res/values/strings.xml | 4 + 8 files changed, 266 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt index b9806da..324578c 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -49,8 +49,6 @@ abstract class TripDatabase : RoomDatabase() { @Module @InstallIn(SingletonComponent::class) object DatabaseModule { - - @RequiresApi(Build.VERSION_CODES.O) @Provides @Singleton @@ -140,12 +138,12 @@ private class DatabasePrepopulator( currency = "USD" ) ) - for (category in sampleCategories) { - categoryDao.insert(category) - } - for (expense in sampleExpenses) { - expenseDao.insert(expense) - } +// for (category in sampleCategories) { +// categoryDao.insert(category) +// } +// for (expense in sampleExpenses) { +// expenseDao.insert(expense) +// } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt index e2e7225..62b9b56 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/entity/Trip.kt @@ -19,6 +19,10 @@ data class Trip( @ColumnInfo("currency") val currency: String, @ColumnInfo("budget") val budget: Double = 0.0 ) { + fun isDummy(): Boolean { + return this.id == -1 + } + companion object { @RequiresApi(Build.VERSION_CODES.O) val DUMMY = Trip( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt index 395b164..b626a64 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt @@ -115,9 +115,9 @@ fun AddExpenseBottomSheet( ) { val currentTripId = currentTrip.id - if (categories.isEmpty()) { - return - } +// if (categories.isEmpty()) { +// return +// } var amount by remember { mutableStateOf( @@ -138,7 +138,11 @@ fun AddExpenseBottomSheet( expenseDtoToEdit?.expense?.currency ?: currentTrip.currency ) } - var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } + var category by remember { + mutableStateOf( + expenseDtoToEdit?.category ?: if (categories.isEmpty()) null else categories[0] + ) + } var datetime by remember { mutableStateOf( expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now() @@ -273,14 +277,14 @@ fun AddExpenseBottomSheet( SaveButton( modifier = Modifier.fillMaxWidth(), - enabled = enableSave, + enabled = enableSave && category != null, onClick = { val expenseToSave = Expense( amount = equationResult, currency = currency, note = note, datetime = datetime, - categoryId = category.id, + categoryId = category!!.id, tripId = currentTripId ) onSave( @@ -410,7 +414,7 @@ fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: Str } @Composable -fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) { +fun CategoryButton(onClick: () -> Unit, category: Category?, modifier: Modifier = Modifier) { Button( contentPadding = PaddingValues(0.dp), onClick = onClick, @@ -422,25 +426,21 @@ fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = contentColor = MaterialTheme.colorScheme.onPrimary ) ) { -// Row(modifier = modifier.fillMaxWidth()) { - Icon( - tint = Color(category.color.toColorInt()), - modifier = Modifier - .size(30.dp) -// .background( -// color = MaterialTheme.colorScheme.prima, -// shape = MaterialTheme.shapes.small -// ) - .padding(end = 10.dp), - painter = painterResource(category.icon.resource), - contentDescription = stringResource(R.string.category), - ) + if (category != null) { + Icon( + tint = Color(category.color.toColorInt()), + modifier = Modifier + .size(30.dp) + .padding(end = 10.dp), + painter = painterResource(category.icon.resource), + contentDescription = stringResource(R.string.category), + ) + } Text( - text = category.name, + text = category?.name ?: stringResource(R.string.pick_category), style = MaterialTheme.typography.titleMedium ) -// } } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt index a7ecf30..1aae1db 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/CategorySelectionDialog.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import cc.n0th1ng.tripmoney.R.* +import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.screens.AddCategoryDialog import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel @@ -35,7 +35,7 @@ import com.composables.icons.materialsymbols.outlined.R fun CategorySelectionDialog( onDismiss: () -> Unit, onCategorySelected: (Category) -> Unit, - selected: Category, + selected: Category?, categories: List, ) { val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt index 8d7a46a..fcc63b8 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt @@ -47,11 +47,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -100,6 +101,7 @@ fun ListExpenseScreen( val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState() ListExpenseScreen( + currentTrip = currentTrip, expensesFlow = expensesFlow, onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, @@ -114,6 +116,7 @@ fun ListExpenseScreen( @RequiresApi(Build.VERSION_CODES.O) @Composable fun ListExpenseScreen( + currentTrip: Trip?, expensesFlow: Flow>, onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit, isRecalculatingRate: Boolean, @@ -137,60 +140,83 @@ fun ListExpenseScreen( var itemToDelete by remember { mutableStateOf(null) } Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { showBottomSheet = true }, - icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) }, - text = { Text(text = stringResource(string.add_expense)) }, - ) + if (currentTrip != null && !currentTrip.isDummy()) { + ExtendedFloatingActionButton( + onClick = { showBottomSheet = true }, + icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) }, + text = { Text(text = stringResource(string.add_expense)) }, + ) + } }) { Box { - LazyColumn( - modifier = Modifier.fillMaxSize().semantics { - contentDescription = "expensesList" - }, - horizontalAlignment = Alignment.CenterHorizontally, - state = listState - ) { - items( - count = items.itemCount, - key = items.itemKey { item -> - when (item) { - is ExpenseListItemUi.Item -> item.expenseDto.expense.id - is ExpenseListItemUi.Header -> "header_${item.date}" - } + if (items.itemCount == 0) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + val textToShow = if (currentTrip == null || currentTrip.isDummy()) { + stringResource(string.no_trip_picked) + } else { + stringResource(string.no_expenses) } - ) { index -> - - when (val item = items[index]) { - - is ExpenseListItemUi.Header -> { - CustomDivider( - date = item.date, - sum = item.sum, - currency = item.currency - ) - } - - is ExpenseListItemUi.Item -> { - SwipeToDeleteExpenseCard( - expenseDto = item.expenseDto, - onDelete = { expense -> itemToDelete = expense }, - onClick = { expenseDto -> - expenseDtoToEdit = expenseDto - showBottomSheet = true - } - ) - } - - null -> {} - - } - Spacer(Modifier.height(10.dp)) - + Text( + text = textToShow, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .semantics { + contentDescription = "expensesList" + }, + horizontalAlignment = Alignment.CenterHorizontally, + state = listState + ) { + items( + count = items.itemCount, + key = items.itemKey { item -> + when (item) { + is ExpenseListItemUi.Item -> item.expenseDto.expense.id + is ExpenseListItemUi.Header -> "header_${item.date}" + } + } + ) { index -> + + when (val item = items[index]) { + + is ExpenseListItemUi.Header -> { + CustomDivider( + date = item.date, + sum = item.sum, + currency = item.currency + ) + } + + is ExpenseListItemUi.Item -> { + SwipeToDeleteExpenseCard( + expenseDto = item.expenseDto, + onDelete = { expense -> itemToDelete = expense }, + onClick = { expenseDto -> + expenseDtoToEdit = expenseDto + showBottomSheet = true + } + ) + } + + null -> {} + + } + Spacer(Modifier.height(10.dp)) + + } + + } } + } if (itemToDelete != null) { @@ -466,6 +492,57 @@ fun PreviewListExpenseScreen() { TripMoneyTheme() { val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList()) ListExpenseScreen( + currentTrip = Trip( + id = 1, + name = "Vacation", + currency = "USD", + startDate = LocalDate.parse("2026-01-01"), + endDate = LocalDate.parse("2026-01-11"), + ), + expensesFlow = MutableStateFlow(pagingData), + onSaveExpense = {}, + onDeleteExpense = {}, + isRecalculatingRate = true, + false, + {} + ) + + } +} + +@RequiresApi(Build.VERSION_CODES.O) +@AllPreviews +@Composable +fun PreviewListExpenseScreenWithoutExpenses() { + TripMoneyTheme() { + val pagingData = PagingData.from(emptyList()) + ListExpenseScreen( + currentTrip = Trip( + id = 1, + name = "Vacation", + currency = "USD", + startDate = LocalDate.parse("2026-01-01"), + endDate = LocalDate.parse("2026-01-11"), + ), + expensesFlow = MutableStateFlow(pagingData), + onSaveExpense = {}, + onDeleteExpense = {}, + isRecalculatingRate = true, + false, + {} + ) + + } +} + +@RequiresApi(Build.VERSION_CODES.O) +@AllPreviews +@Composable +fun PreviewListExpenseScreenWithoutTrip() { + TripMoneyTheme() { + val pagingData = PagingData.from(emptyList()) + ListExpenseScreen( + currentTrip = null, expensesFlow = MutableStateFlow(pagingData), onSaveExpense = {}, onDeleteExpense = {}, @@ -480,7 +557,7 @@ fun PreviewListExpenseScreen() { @AllPreviews @Composable fun PreviewDeleteConfirmationDialog() { - TripMoneyTheme() { + TripMoneyTheme { DeleteConfirmationDialog( onConfirm = {}, onCancel = {}) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt index 3fa4ae3..88d1f63 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/statistics/StatisticsScreen.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.graphics.toColorInt import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -85,7 +87,9 @@ fun StatisticsScreen( ) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Summary( - Modifier.weight(1f), -1 * summaryAmount, tripCurrency.name, + Modifier.weight(1f), + if (summaryAmount == 0.0) 0.0 else -1 * summaryAmount, + tripCurrency.name, stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses), R.drawable.materialsymbols_ic_payment_arrow_down_outlined, iconColor = MaterialTheme.colorScheme.error @@ -159,19 +163,31 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List) { colors = CardDefaults.elevatedCardColors() .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) ) { - Column( - modifier = Modifier - .padding(15.dp) - .verticalScroll(rememberScrollState()), - - verticalArrangement = Arrangement.spacedBy(5.dp) - ) { - summaryPerCategoryList.forEach { - CategoryCard( - summaryPerCategory = it, modifier = Modifier - .fillMaxWidth() + if (summaryPerCategoryList.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(cc.n0th1ng.tripmoney.R.string.no_expenses_summary), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) ) } + } else { + Column( + modifier = Modifier + .padding(15.dp) + .verticalScroll(rememberScrollState()), + + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + summaryPerCategoryList.forEach { + CategoryCard( + summaryPerCategory = it, modifier = Modifier + .fillMaxWidth() + ) + } + } } } } @@ -238,7 +254,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa @RequiresApi(Build.VERSION_CODES.O) @AllPreviews @Composable -fun Preview() { +fun PreviewStatisticScreen() { TripMoneyTheme { Scaffold { StatisticsScreen( @@ -251,6 +267,25 @@ fun Preview() { } } +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@RequiresApi(Build.VERSION_CODES.O) +@AllPreviews +@Composable +fun PreviewStatisticScreenWithNoData() { + TripMoneyTheme { + Scaffold { + StatisticsScreen( + emptyList(), + summaryAmount = 0.0, + Currencies.entries.random(), + null + ) + } + } +} + + + val categories = listOf( Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()), Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()), diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt index a42ac7c..eb3536e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/trippicker/TripPickerScreen.kt @@ -38,14 +38,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import androidx.paging.PagingData @@ -113,32 +112,45 @@ fun TripPickerScreen( Icon(Icons.Filled.Add, stringResource(string.add_trip)) } }) { paddingValues -> - LazyColumn( - modifier = Modifier - .padding(horizontal = 15.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.Center - ) { - items(trips.itemCount, trips.itemKey { it.id }) { i -> - Spacer(Modifier.height(10.dp)) - val trip = trips[i] - if (trip != null) { - SwipeToDeleteTripCard( - trip = trip, - onDelete = { - onDelete(trip) - }, onClick = { - onClick(trip) - }, isSelected = currentTripId == trip.id, - onLongClick = { trip -> - tripToEdit = trip - showBottomSheet = true - }) + if (trips.itemCount == 0) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(string.no_trip_added), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Light, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + } else { + LazyColumn( + modifier = Modifier + .padding(horizontal = 15.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + items(trips.itemCount, trips.itemKey { it.id }) { i -> + Spacer(Modifier.height(10.dp)) + val trip = trips[i] + if (trip != null) { + SwipeToDeleteTripCard( + trip = trip, + onDelete = { + onDelete(trip) + }, onClick = { + onClick(trip) + }, isSelected = currentTripId == trip.id, + onLongClick = { trip -> + tripToEdit = trip + showBottomSheet = true + }) + } + Spacer(Modifier.height(10.dp)) } - Spacer(Modifier.height(10.dp)) } } + if (showBottomSheet) { AddTripBottomSheet( onDismiss = { @@ -250,7 +262,8 @@ fun TripCard( } Column( modifier = Modifier.padding(end = 20.dp), - horizontalAlignment = Alignment.End) { + horizontalAlignment = Alignment.End + ) { Text( trip.currency.uppercase(), style = MaterialTheme.typography.titleLarge, @@ -307,4 +320,20 @@ fun PreviewTripPickerScreen() { onSave = {} ) } +} + +@RequiresApi(Build.VERSION_CODES.O) +@AllPreviews +@Composable +fun PreviewTripPickerScreenNoTrip() { + + TripMoneyTheme { + TripPickerScreen( + tripsFlow = MutableStateFlow(PagingData.from(emptyList())), + currentTripId = 1, + onDelete = {}, + onClick = {}, + onSave = {} + ) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 182c496..de147f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,4 +41,8 @@ Open add expense form on startup Yesterday Clear + Start budgeting by adding expenses + Select trip to see expenses + Start budgeting by adding your trip + No expenses to summarize \ No newline at end of file