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
@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)
// }
}

View File

@@ -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(

View File

@@ -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<Category?>(
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
)
// }
}
}

View File

@@ -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<Category>,
) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()

View File

@@ -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<PagingData<ExpenseListItemUi>>,
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit,
isRecalculatingRate: Boolean,
@@ -137,60 +140,83 @@ fun ListExpenseScreen(
var itemToDelete by remember { mutableStateOf<Expense?>(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<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),
onSaveExpense = {},
onDeleteExpense = {},
@@ -480,7 +557,7 @@ fun PreviewListExpenseScreen() {
@AllPreviews
@Composable
fun PreviewDeleteConfirmationDialog() {
TripMoneyTheme() {
TripMoneyTheme {
DeleteConfirmationDialog(
onConfirm = {},
onCancel = {})

View File

@@ -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<SummaryPerCategory>) {
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()),

View File

@@ -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,
@@ -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="yesterday">Yesterday</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>