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 324578c..d656b1f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -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) + } } diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt index 4a4c399..b7300f8 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dto/SummaryPerCategory.kt @@ -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 +) \ No newline at end of file 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 fcc63b8..16adc81 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 @@ -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 { diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/managecategories/ManageCategoriesScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/managecategories/ManageCategoriesScreen.kt index 5e30ae1..1bc595a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/managecategories/ManageCategoriesScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/managecategories/ManageCategoriesScreen.kt @@ -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 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 88d1f63..9370422 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 @@ -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, + summaryPerDayList: List, 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) { +fun SummaryPerCategoryCard( + summaryPerCategoryList: List, + 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) { } } +@RequiresApi(Build.VERSION_CODES.O) +@Composable +fun SummaryPerDayCard(modifier: Modifier = Modifier, summaryPerDayList: List) { + 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), -) \ No newline at end of file +) + +@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() } \ No newline at end of file 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 eb3536e..4cf41a2 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 @@ -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, diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt index e1e5376..0bdeae8 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -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> = + fun getExpensesDtoPaged( + tripId: Int, + search: String = "", + filter: Filter = Filter() + ): Flow> = 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> = + fun getExpensesDto( + tripId: Int, + search: String = "", + filter: Filter = Filter() + ): Flow> = 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> { + 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 { diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c19f565..998995d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -32,4 +32,17 @@ Zapisz wydatki z %s do pliku Dodaj kategorie Edytuj kategorie + Archiwizuj + Chcesz zarchiwizować? + Żadne wydatki nie będą usunięte. + Wszystkie wydatki z kategorii %s zostaną usunięte. + Budżet + Pozostałe środki + Pokaż dodawanie wydatku na starcie + Wczoraj + Wyczyść + Zacznij budżetowanie od dodania wydatków + Wybierz wycieczkę żeby zobaczyć wydatki + Zacznij budżetowanie od dodania wycieczki + Brak wydatków do podsumowania \ 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 de147f1..e5c914a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,7 +35,7 @@ Archive Do you want to archive? No expense will be deleted. - Your all expenses with category Hotel will be removed. + Your all expenses with category %s will be removed. Budget Money left Open add expense form on startup