fix: add info text when no data

This commit is contained in:
Rafal Wisniewski
2026-05-01 13:10:27 +02:00
parent dfe9dbd08b
commit 3286bcf87a
8 changed files with 266 additions and 119 deletions

View File

@@ -49,8 +49,6 @@ abstract class TripDatabase : RoomDatabase() {
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object DatabaseModule { object DatabaseModule {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Provides @Provides
@Singleton @Singleton
@@ -140,12 +138,12 @@ private class DatabasePrepopulator(
currency = "USD" currency = "USD"
) )
) )
for (category in sampleCategories) { // for (category in sampleCategories) {
categoryDao.insert(category) // categoryDao.insert(category)
} // }
for (expense in sampleExpenses) { // for (expense in sampleExpenses) {
expenseDao.insert(expense) // expenseDao.insert(expense)
} // }
} }

View File

@@ -19,6 +19,10 @@ data class Trip(
@ColumnInfo("currency") val currency: String, @ColumnInfo("currency") val currency: String,
@ColumnInfo("budget") val budget: Double = 0.0 @ColumnInfo("budget") val budget: Double = 0.0
) { ) {
fun isDummy(): Boolean {
return this.id == -1
}
companion object { companion object {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
val DUMMY = Trip( val DUMMY = Trip(

View File

@@ -115,9 +115,9 @@ fun AddExpenseBottomSheet(
) { ) {
val currentTripId = currentTrip.id val currentTripId = currentTrip.id
if (categories.isEmpty()) { // if (categories.isEmpty()) {
return // return
} // }
var amount by remember { var amount by remember {
mutableStateOf( mutableStateOf(
@@ -138,7 +138,11 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit?.expense?.currency ?: currentTrip.currency expenseDtoToEdit?.expense?.currency ?: currentTrip.currency
) )
} }
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) } var category by remember {
mutableStateOf<Category?>(
expenseDtoToEdit?.category ?: if (categories.isEmpty()) null else categories[0]
)
}
var datetime by remember { var datetime by remember {
mutableStateOf( mutableStateOf(
expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now() expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now()
@@ -273,14 +277,14 @@ fun AddExpenseBottomSheet(
SaveButton( SaveButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = enableSave, enabled = enableSave && category != null,
onClick = { onClick = {
val expenseToSave = Expense( val expenseToSave = Expense(
amount = equationResult, amount = equationResult,
currency = currency, currency = currency,
note = note, note = note,
datetime = datetime, datetime = datetime,
categoryId = category.id, categoryId = category!!.id,
tripId = currentTripId tripId = currentTripId
) )
onSave( onSave(
@@ -410,7 +414,7 @@ fun CurrencyButton(modifier: Modifier = Modifier, onClick: () -> Unit, text: Str
} }
@Composable @Composable
fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier = Modifier) { fun CategoryButton(onClick: () -> Unit, category: Category?, modifier: Modifier = Modifier) {
Button( Button(
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
onClick = onClick, onClick = onClick,
@@ -422,25 +426,21 @@ fun CategoryButton(onClick: () -> Unit, category: Category, modifier: Modifier =
contentColor = MaterialTheme.colorScheme.onPrimary contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { ) {
// Row(modifier = modifier.fillMaxWidth()) { if (category != null) {
Icon( Icon(
tint = Color(category.color.toColorInt()), tint = Color(category.color.toColorInt()),
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
// .background(
// color = MaterialTheme.colorScheme.prima,
// shape = MaterialTheme.shapes.small
// )
.padding(end = 10.dp), .padding(end = 10.dp),
painter = painterResource(category.icon.resource), painter = painterResource(category.icon.resource),
contentDescription = stringResource(R.string.category), contentDescription = stringResource(R.string.category),
) )
}
Text( Text(
text = category.name, text = category?.name ?: stringResource(R.string.pick_category),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
// }
} }
} }

View File

@@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.data.entity.Category
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
@@ -35,7 +35,7 @@ import com.composables.icons.materialsymbols.outlined.R
fun CategorySelectionDialog( fun CategorySelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onCategorySelected: (Category) -> Unit, onCategorySelected: (Category) -> Unit,
selected: Category, selected: Category?,
categories: List<Category>, categories: List<Category>,
) { ) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()

View File

@@ -47,11 +47,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics 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.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -100,6 +101,7 @@ fun ListExpenseScreen(
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState() val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
ListExpenseScreen( ListExpenseScreen(
currentTrip = currentTrip,
expensesFlow = expensesFlow, expensesFlow = expensesFlow,
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) }, onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) },
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) }, onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
@@ -114,6 +116,7 @@ fun ListExpenseScreen(
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Composable @Composable
fun ListExpenseScreen( fun ListExpenseScreen(
currentTrip: Trip?,
expensesFlow: Flow<PagingData<ExpenseListItemUi>>, expensesFlow: Flow<PagingData<ExpenseListItemUi>>,
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit, onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit,
isRecalculatingRate: Boolean, isRecalculatingRate: Boolean,
@@ -137,16 +140,37 @@ fun ListExpenseScreen(
var itemToDelete by remember { mutableStateOf<Expense?>(null) } var itemToDelete by remember { mutableStateOf<Expense?>(null) }
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = { Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
if (currentTrip != null && !currentTrip.isDummy()) {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { showBottomSheet = true }, onClick = { showBottomSheet = true },
icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) }, icon = { Icon(Icons.Filled.Add, stringResource(string.add_expense)) },
text = { Text(text = stringResource(string.add_expense)) }, text = { Text(text = stringResource(string.add_expense)) },
) )
}
}) })
{ {
Box { Box {
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)
}
Text(
text = textToShow,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
} else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize().semantics { modifier = Modifier
.fillMaxSize()
.semantics {
contentDescription = "expensesList" contentDescription = "expensesList"
}, },
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -193,6 +217,8 @@ fun ListExpenseScreen(
} }
} }
}
if (itemToDelete != null) { if (itemToDelete != null) {
DeleteConfirmationDialog( DeleteConfirmationDialog(
onConfirm = { onConfirm = {
@@ -466,6 +492,57 @@ fun PreviewListExpenseScreen() {
TripMoneyTheme() { TripMoneyTheme() {
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList()) val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
ListExpenseScreen( 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<ExpenseListItemUi>())
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<ExpenseListItemUi>())
ListExpenseScreen(
currentTrip = null,
expensesFlow = MutableStateFlow(pagingData), expensesFlow = MutableStateFlow(pagingData),
onSaveExpense = {}, onSaveExpense = {},
onDeleteExpense = {}, onDeleteExpense = {},
@@ -480,7 +557,7 @@ fun PreviewListExpenseScreen() {
@AllPreviews @AllPreviews
@Composable @Composable
fun PreviewDeleteConfirmationDialog() { fun PreviewDeleteConfirmationDialog() {
TripMoneyTheme() { TripMoneyTheme {
DeleteConfirmationDialog( DeleteConfirmationDialog(
onConfirm = {}, onConfirm = {},
onCancel = {}) onCancel = {})

View File

@@ -32,6 +32,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -85,7 +87,9 @@ fun StatisticsScreen(
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Summary( 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), stringResource(cc.n0th1ng.tripmoney.R.string.total_expenses),
R.drawable.materialsymbols_ic_payment_arrow_down_outlined, R.drawable.materialsymbols_ic_payment_arrow_down_outlined,
iconColor = MaterialTheme.colorScheme.error iconColor = MaterialTheme.colorScheme.error
@@ -159,6 +163,17 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
colors = CardDefaults.elevatedCardColors() colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer) .copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) { ) {
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( Column(
modifier = Modifier modifier = Modifier
.padding(15.dp) .padding(15.dp)
@@ -175,6 +190,7 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
} }
} }
} }
}
@Composable @Composable
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) { fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
@@ -238,7 +254,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@AllPreviews @AllPreviews
@Composable @Composable
fun Preview() { fun PreviewStatisticScreen() {
TripMoneyTheme { TripMoneyTheme {
Scaffold { Scaffold {
StatisticsScreen( 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( val categories = listOf(
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()), Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()), Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()),

View File

@@ -38,14 +38,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.paging.PagingData import androidx.paging.PagingData
@@ -113,6 +112,17 @@ fun TripPickerScreen(
Icon(Icons.Filled.Add, stringResource(string.add_trip)) Icon(Icons.Filled.Add, stringResource(string.add_trip))
} }
}) { paddingValues -> }) { paddingValues ->
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( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(horizontal = 15.dp) .padding(horizontal = 15.dp)
@@ -138,6 +148,8 @@ fun TripPickerScreen(
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
} }
} }
}
if (showBottomSheet) { if (showBottomSheet) {
AddTripBottomSheet( AddTripBottomSheet(
@@ -250,7 +262,8 @@ fun TripCard(
} }
Column( Column(
modifier = Modifier.padding(end = 20.dp), modifier = Modifier.padding(end = 20.dp),
horizontalAlignment = Alignment.End) { horizontalAlignment = Alignment.End
) {
Text( Text(
trip.currency.uppercase(), trip.currency.uppercase(),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@@ -308,3 +321,19 @@ fun PreviewTripPickerScreen() {
) )
} }
} }
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewTripPickerScreenNoTrip() {
TripMoneyTheme {
TripPickerScreen(
tripsFlow = MutableStateFlow(PagingData.from(emptyList())),
currentTripId = 1,
onDelete = {},
onClick = {},
onSave = {}
)
}
}

View File

@@ -41,4 +41,8 @@
<string name="add_expense_settings">Open add expense form on startup</string> <string name="add_expense_settings">Open add expense form on startup</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="no_expenses">Start budgeting by adding expenses</string>
<string name="no_trip_picked">Select trip to see expenses</string>
<string name="no_trip_added">Start budgeting by adding your trip</string>
<string name="no_expenses_summary">No expenses to summarize</string>
</resources> </resources>