feat: add categories per day stats
This commit is contained in:
@@ -138,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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package cc.n0th1ng.tripmoney.data.dto
|
||||
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import java.time.LocalDate
|
||||
|
||||
data class SummaryPerCategory(
|
||||
val category: Category,
|
||||
@@ -11,12 +11,8 @@ data class SummaryPerCategory(
|
||||
val currency: Currencies
|
||||
)
|
||||
|
||||
data class SummaryPerCategoryRaw(
|
||||
val categoryId: Int,
|
||||
val categoryName: String,
|
||||
val icon: Icons,
|
||||
val color: String,
|
||||
data class SummaryPerDay(
|
||||
val day: LocalDate,
|
||||
val amount: Double,
|
||||
val currency: String
|
||||
)
|
||||
|
||||
val percent: Float
|
||||
)
|
||||
@@ -151,7 +151,7 @@ fun ListExpenseScreen(
|
||||
{
|
||||
Box {
|
||||
if (items.itemCount == 0) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = Alignment.Center) {
|
||||
val textToShow = if (currentTrip == null || currentTrip.isDummy()) {
|
||||
stringResource(string.no_trip_picked)
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -20,7 +19,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
@@ -48,7 +46,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -63,9 +60,6 @@ import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import cc.n0th1ng.tripmoney.utils.colors
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.collections.emptyList
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
@@ -159,7 +153,7 @@ fun ManageCategoriesScreen(
|
||||
|
||||
if (itemToDelete != null) {
|
||||
DeleteConfirmationDialog(
|
||||
bodyText = stringResource(string.delete_category_info),
|
||||
bodyText = stringResource(string.delete_category_info).format(itemToDelete?.name),
|
||||
onConfirm = {
|
||||
onDeleteCategory(itemToDelete!!)
|
||||
itemToDelete = null
|
||||
|
||||
@@ -4,15 +4,19 @@ import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -35,9 +39,11 @@ 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.core.graphics.toColorInt
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerDay
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
@@ -49,6 +55,8 @@ import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
@@ -60,11 +68,14 @@ fun StatisticsScreen() {
|
||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
|
||||
.collectAsState(emptyList())
|
||||
val summaryPerDayList by expenseAndCategoryViewModel.getSummaryPerDay(currentTripId)
|
||||
.collectAsState(emptyList())
|
||||
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
|
||||
.collectAsState(0.0)
|
||||
val moneyLeft by expenseAndCategoryViewModel.getBudgetLeft(currentTripId).collectAsState(null)
|
||||
StatisticsScreen(
|
||||
summaryPerCategoryList,
|
||||
summaryPerDayList,
|
||||
summaryAmount,
|
||||
Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name),
|
||||
moneyLeft
|
||||
@@ -75,6 +86,7 @@ fun StatisticsScreen() {
|
||||
@Composable
|
||||
fun StatisticsScreen(
|
||||
summaryPerCategoryList: List<SummaryPerCategory>,
|
||||
summaryPerDayList: List<SummaryPerDay>,
|
||||
summaryAmount: Double,
|
||||
tripCurrency: Currencies,
|
||||
moneyLeft: Double?
|
||||
@@ -101,8 +113,11 @@ fun StatisticsScreen(
|
||||
iconColor = colorResource(cc.n0th1ng.tripmoney.R.color.good_green)
|
||||
)
|
||||
}
|
||||
SummaryPerCategoryCard(summaryPerCategoryList)
|
||||
|
||||
SummaryPerCategoryCard(
|
||||
modifier = Modifier.heightIn(max = 300.dp),
|
||||
summaryPerCategoryList = summaryPerCategoryList
|
||||
)
|
||||
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,14 +172,22 @@ fun Summary(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
|
||||
fun SummaryPerCategoryCard(
|
||||
summaryPerCategoryList: List<SummaryPerCategory>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors()
|
||||
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
if (summaryPerCategoryList.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(cc.n0th1ng.tripmoney.R.string.no_expenses_summary),
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -192,6 +215,47 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun SummaryPerDayCard(modifier: Modifier = Modifier, summaryPerDayList: List<SummaryPerDay>) {
|
||||
ElevatedCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors()
|
||||
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
if (summaryPerDayList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
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 {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(15.dp)
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
summaryPerDayList.forEach {
|
||||
DayCard(
|
||||
summaryPerDay = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -250,6 +314,67 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun DayCard(modifier: Modifier = Modifier, summaryPerDay: SummaryPerDay) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = "%.2f".format(summaryPerDay.amount),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontSize = (MaterialTheme.typography.labelSmall.fontSize.value - 2).sp,
|
||||
)
|
||||
val width = 45.dp
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(width)
|
||||
.fillMaxHeight(0.2f + (0.98f - 0.2f) * summaryPerDay.percent)
|
||||
.clip(RoundedCornerShape(width / 2))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.padding(top = 5.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.size(width - 10.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = RoundedCornerShape(width / 2)
|
||||
)
|
||||
.padding(vertical = 3.dp),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
text = summaryPerDay.day.format(DateTimeFormatter.ofPattern("dd"))
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Light,
|
||||
fontSize = (MaterialTheme.typography.labelSmall.fontSize.value - 2).sp,
|
||||
lineHeight = 10.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
text = summaryPerDay.day.format(DateTimeFormatter.ofPattern("E"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@@ -259,6 +384,7 @@ fun PreviewStatisticScreen() {
|
||||
Scaffold {
|
||||
StatisticsScreen(
|
||||
summaryPerCategoryList,
|
||||
summaryPerDayList,
|
||||
summaryAmount = 125.24,
|
||||
Currencies.entries.random(),
|
||||
432.14
|
||||
@@ -275,6 +401,7 @@ fun PreviewStatisticScreenWithNoData() {
|
||||
TripMoneyTheme {
|
||||
Scaffold {
|
||||
StatisticsScreen(
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
summaryAmount = 0.0,
|
||||
Currencies.entries.random(),
|
||||
@@ -285,7 +412,6 @@ fun PreviewStatisticScreenWithNoData() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
val categories = listOf(
|
||||
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
|
||||
Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()),
|
||||
@@ -303,4 +429,26 @@ val summaryPerCategoryList = listOf(
|
||||
SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN),
|
||||
)
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val summaryPerDayListRaw = listOf(
|
||||
SummaryPerDay(LocalDate.now(), 50.0, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(1), 500.23, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(2), 1560.53, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(3), 700.32, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(4), 201.3, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(5), 2020.64, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(6), 510.43, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(7), 3050.12, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(8), 264.32, 0f),
|
||||
SummaryPerDay(LocalDate.now().minusDays(9), 3596.64, 0f)
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val highestAmount = summaryPerDayListRaw.maxOf { it.amount }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val summaryPerDayList = summaryPerDayListRaw.map {
|
||||
it.copy(percent = ((it.amount / highestAmount)).toFloat())
|
||||
}.sortedBy { it.day.toEpochDay() }
|
||||
@@ -113,7 +113,7 @@ fun TripPickerScreen(
|
||||
}
|
||||
}) { paddingValues ->
|
||||
if (trips.itemCount == 0) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Box(modifier = Modifier.fillMaxSize().padding(10.dp), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = stringResource(string.no_trip_added),
|
||||
textAlign = TextAlign.Center,
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.paging.insertSeparators
|
||||
import androidx.paging.map
|
||||
import cc.n0th1ng.tripmoney.Filter
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerDay
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
@@ -44,7 +45,11 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
return expenseRepo.getBudgetLeft(tripId)
|
||||
}
|
||||
|
||||
fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<PagingData<ExpenseDto>> =
|
||||
fun getExpensesDtoPaged(
|
||||
tripId: Int,
|
||||
search: String = "",
|
||||
filter: Filter = Filter()
|
||||
): Flow<PagingData<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@@ -86,7 +91,11 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}.cachedIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<List<ExpenseDto>> =
|
||||
fun getExpensesDto(
|
||||
tripId: Int,
|
||||
search: String = "",
|
||||
filter: Filter = Filter()
|
||||
): Flow<List<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDto(tripId, search, filter)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@@ -184,6 +193,30 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getSummaryPerDay(tripId: Int): Flow<List<SummaryPerDay>> {
|
||||
val tripFlow = tripRepo.getTrip(tripId)
|
||||
val expensesFlow = getExpensesDto(tripId)
|
||||
|
||||
return tripFlow.combine(expensesFlow) { trip, expenses ->
|
||||
val summaryPerDayRaw = expenses.groupBy { it.expense.datetime.toLocalDate() }
|
||||
.map { (day, expensesForDay) ->
|
||||
val total = expensesForDay.sumOf { it.expense.convertedAmount() }
|
||||
SummaryPerDay(
|
||||
amount = total,
|
||||
day = day,
|
||||
percent = 0.0f
|
||||
)
|
||||
}
|
||||
.sortedByDescending { it.day }
|
||||
|
||||
val highestAmount = summaryPerDayRaw.maxOf { it.amount }
|
||||
summaryPerDayRaw.map {
|
||||
it.copy(percent = ((it.amount / highestAmount)).toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun clearOldRates() {
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -32,4 +32,17 @@
|
||||
<string name="export_csv_subttext">Zapisz wydatki z %s do pliku</string>
|
||||
<string name="add_new_category">Dodaj kategorie</string>
|
||||
<string name="edit_category">Edytuj kategorie</string>
|
||||
<string name="archive">Archiwizuj</string>
|
||||
<string name="you_want_archive">Chcesz zarchiwizować?</string>
|
||||
<string name="archive_category_info">Żadne wydatki nie będą usunięte.</string>
|
||||
<string name="delete_category_info">Wszystkie wydatki z kategorii %s zostaną usunięte.</string>
|
||||
<string name="budget">Budżet</string>
|
||||
<string name="money_left">Pozostałe środki</string>
|
||||
<string name="add_expense_settings">Pokaż dodawanie wydatku na starcie</string>
|
||||
<string name="yesterday">Wczoraj</string>
|
||||
<string name="clear">Wyczyść</string>
|
||||
<string name="no_expenses">Zacznij budżetowanie od dodania wydatków</string>
|
||||
<string name="no_trip_picked">Wybierz wycieczkę żeby zobaczyć wydatki</string>
|
||||
<string name="no_trip_added">Zacznij budżetowanie od dodania wycieczki</string>
|
||||
<string name="no_expenses_summary">Brak wydatków do podsumowania</string>
|
||||
</resources>
|
||||
@@ -35,7 +35,7 @@
|
||||
<string name="archive">Archive</string>
|
||||
<string name="you_want_archive">Do you want to archive?</string>
|
||||
<string name="archive_category_info">No expense will be deleted.</string>
|
||||
<string name="delete_category_info">Your all expenses with category Hotel will be removed.</string>
|
||||
<string name="delete_category_info">Your all expenses with category %s will be removed.</string>
|
||||
<string name="budget">Budget</string>
|
||||
<string name="money_left">Money left</string>
|
||||
<string name="add_expense_settings">Open add expense form on startup</string>
|
||||
|
||||
Reference in New Issue
Block a user