Compare commits

..

6 Commits

Author SHA1 Message Date
Rafal Wisniewski
ae5394aa59 fix: add default colors and icons 2026-05-06 14:01:46 +02:00
Rafal Wisniewski
dae0212cf9 fix: sort stats per day 2026-05-06 13:03:17 +02:00
Rafal Wisniewski
270ff4fa07 fix: add date to filter 2026-05-06 12:47:11 +02:00
Rafal Wisniewski
f83bf62655 fix: add search to category picker 2026-05-06 10:49:23 +02:00
Rafal Wisniewski
6c067f64ce fix: add search to category picker 2026-05-06 10:49:09 +02:00
Rafal Wisniewski
79551ab69d fix: search & filter on list expense screen 2026-05-04 22:01:38 +02:00
19 changed files with 340 additions and 193 deletions

View File

@@ -15,7 +15,7 @@ android {
}
defaultConfig {
applicationId = "cc.n0th1ng.tripmoney"
minSdk = 24
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"

View File

@@ -44,6 +44,7 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.time.LocalDate
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@@ -57,7 +58,6 @@ class MainActivity : ComponentActivity() {
NavigationDrawer()
}
}
}
}
@@ -97,7 +97,7 @@ fun NavigationDrawer() {
}
}
},
isSearchable = current == Screens.LIST_EXPENSE,
isSearchable = current != null && current.contains(Screens.LIST_EXPENSE),
onSearchChange = { newSearch -> search = newSearch },
onFilterChange = { newFilter -> filter = newFilter },
categories = categories,
@@ -111,14 +111,16 @@ fun NavigationDrawer() {
startDestination = if (currentTripId == -1) Screens.TRIP_PICKER else Screens.LIST_EXPENSE,
modifier = Modifier.padding(innerPadding)
) {
composable(Screens.LIST_EXPENSE+"?dateToScroll={dateToScroll}",
arguments = listOf(navArgument("dateToScroll"){defaultValue = ""})) {
backStackEntry ->
composable(
Screens.LIST_EXPENSE + "?dateToScroll={dateToScroll}",
arguments = listOf(navArgument("dateToScroll") { defaultValue = "" })
) { backStackEntry ->
ListExpenseScreen(
filter = filter, search = search,
initialAutoOpen = shouldTriggerAutoOpen,
onAutoOpenConsumed = { hasHandledStartupOpen = true },
dateToScroll = backStackEntry.arguments?.getString("dateToScroll")?: "")
dateToScroll = backStackEntry.arguments?.getString("dateToScroll") ?: ""
)
}
composable(Screens.TRIP_PICKER) {
TripPickerScreen(navController)
@@ -139,7 +141,9 @@ fun NavigationDrawer() {
data class Filter(
val categories: List<Category> = emptyList(), val startAmount: Double = 0.0,
val endAmount: Double = Double.MAX_VALUE
val endAmount: Double = Double.MAX_VALUE,
val startDate: LocalDate = LocalDate.MIN,
val endDate: LocalDate = LocalDate.MAX
) {
fun with(category: Category): Filter {
return this.copy(categories = categories + category)
@@ -153,11 +157,19 @@ data class Filter(
return this.copy(endAmount = amount)
}
fun withStartDate(date: LocalDate): Filter {
return this.copy(startDate = date)
}
fun withEndDate(date: LocalDate): Filter {
return this.copy(endDate = date)
}
fun without(category: Category): Filter {
return this.copy(categories = categories - category)
}
fun isDefault(): Boolean {
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE && startDate == LocalDate.MIN && endDate == LocalDate.MAX
}
}

View File

@@ -153,16 +153,16 @@ private class DatabasePrepopulator(
name = "Hotel", icon = Icons.HOTEL, color = colors.random()
),
Category(
name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()
name = "Restaurants", icon = Icons.RESTAURANT, color = colors.random()
),
Category(
name = "Transport", icon = Icons.FLIGHT, color = colors.random()
),
Category(
name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()
name = "Entertainment", icon = Icons.ATTRACTION, color = colors.random()
),
Category(
name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()
name = "Groceries", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()

View File

@@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
@@ -21,7 +20,7 @@ interface CategoryDao {
@Transaction
@Query(
"""
SELECT * FROM category WHERE archived is 0
SELECT * FROM category WHERE archived is 0 ORDER BY name
"""
)
fun categories(): Flow<List<Category>>

View File

@@ -40,6 +40,12 @@ interface ExpenseDao {
AND (
:endAmount IS NULL OR expense.amount <= :endAmount
)
AND (
:startDate IS NULL OR expense.datetime >= :startDate
)
AND (
:endDate IS NULL OR expense.datetime <= :endDate
)
ORDER BY expense.datetime DESC
"""
@@ -50,7 +56,9 @@ interface ExpenseDao {
categoryIds: List<Int>,
categoriesEmpty: Boolean,
startAmount: Double?,
endAmount: Double?
endAmount: Double?,
startDate: Long?,
endDate: Long?
): PagingSource<Int, ExpenseDto>
@Transaction
@@ -74,7 +82,12 @@ interface ExpenseDao {
AND (
:endAmount IS NULL OR expense.amount <= :endAmount
)
AND (
:startDate IS NULL OR expense.datetime >= :startDate
)
AND (
:endDate IS NULL OR expense.datetime <= :endDate
)
ORDER BY expense.datetime DESC
"""
)
@@ -84,7 +97,9 @@ interface ExpenseDao {
categoryIds: List<Int>,
categoriesEmpty: Boolean,
startAmount: Double?,
endAmount: Double?
endAmount: Double?,
startDate: Long?,
endDate: Long?
): Flow<List<ExpenseDto>>
@Query(

View File

@@ -1,7 +1,5 @@
package cc.n0th1ng.tripmoney.data.repository
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.paging.Pager
import androidx.paging.PagingConfig
@@ -10,9 +8,11 @@ import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.screens.listexpense.toEpochMilli
import cc.n0th1ng.tripmoney.utils.Currencies
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import java.time.LocalDate
import javax.inject.Inject
class ExpenseRepository @Inject constructor(
@@ -49,9 +49,10 @@ class ExpenseRepository @Inject constructor(
categoryIds = categoryIds,
categoriesEmpty = categoryIds.isEmpty(),
startAmount = filter.startAmount,
endAmount = filter.endAmount
endAmount = filter.endAmount,
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
)
}
).flow
}
@@ -62,18 +63,18 @@ class ExpenseRepository @Inject constructor(
filter: Filter = Filter()
): Flow<List<ExpenseDto>> {
val categoryIds = filter.categories.map { it.id }
return expenseDao.expenseDto(
tripId = tripId,
search = search.takeIf { it.isNotBlank() },
categoryIds = categoryIds,
categoriesEmpty = categoryIds.isEmpty(),
startAmount = filter.startAmount,
endAmount = filter.endAmount
endAmount = filter.endAmount,
startDate = if(filter.startDate == LocalDate.MIN) null else filter.startDate.toEpochMilli(),
endDate = if(filter.endDate == LocalDate.MAX) null else filter.endDate.plusDays(1).toEpochMilli(),
)
}
@RequiresApi(Build.VERSION_CODES.O)
suspend fun recalculateTripExpenses(tripId: Int) {
val expenses = getExpensesDto(tripId).first()
expenses.forEach { expenseDto ->

View File

@@ -2,8 +2,6 @@ package cc.n0th1ng.tripmoney.navigation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -12,11 +10,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -44,6 +38,7 @@ import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.screens.listexpense.FilterDialog
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import com.composables.icons.materialsymbols.outlined.R.drawable
@@ -159,71 +154,9 @@ fun TopBar(
showFilter = false
}
)
}
}
@Composable
fun FilterDialog(
onDismiss: () -> Unit,
onSave: (Filter) -> Unit,
onClear: () -> Unit,
categories: List<Category>,
filter: Filter
) {
var filter by remember { mutableStateOf(filter) }
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onClear
) { Text(stringResource(R.string.clear)) }
},
confirmButton = {
Button(
enabled = true,
onClick = {
onSave(
filter.withStartAmount(fromAmountString.safeToDouble())
.withEndAmount(toAmountString.safeToDouble())
)
}) { Text(stringResource(R.string.save)) }
}, title = { Text("Filter") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Categories")
FlowRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
categories.forEach {
FilterChip(selected = filter.categories.contains(it), onClick = {
filter = if (filter.categories.contains(it)) {
filter.without(it)
} else {
filter.with(it)
}
}, label = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(painterResource(it.icon.resource), contentDescription = null)
Text(text = it.name)
}
})
}
}
AmountTextField(label = "from", onValueChange = { newText ->
fromAmountString = newText
}, value = fromAmountString)
AmountTextField(label = "to", onValueChange = { newText ->
toAmountString = newText
}, value = toAmountString)
}
})
}
@Composable
fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) {
var value by remember { mutableStateOf(value) }
@@ -277,23 +210,3 @@ fun PreviewTopBar() {
)
}
}
@AllPreviews
@Composable
fun PreviewFilterDialog() {
TripMoneyTheme {
FilterDialog(
onDismiss = {},
onSave = {},
categories = categoriesToPreview,
filter = Filter(),
onClear = {}
)
}
}
private fun String.safeToDouble(): Double {
if (this == "") return Double.MAX_VALUE
if (this.isEmpty()) return 0.0
return this.toDouble()
}

View File

@@ -98,7 +98,8 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit = expenseDtoToEdit,
state = state,
currentTrip = currentTrip ?: Trip.DUMMY,
categories = categories
categories = categories,
onSaveCategory = {expenseAndCategoryViewModel.save(it)}
)
}
@@ -111,7 +112,8 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit: ExpenseDto?,
state: SheetState,
currentTrip: Trip,
categories: List<Category>
categories: List<Category>,
onSaveCategory: (Category) -> Unit
) {
val currentTripId = currentTrip.id
@@ -319,7 +321,8 @@ fun AddExpenseBottomSheet(
category = selectedCategory
},
selected = category,
categories = categories
categories = categories,
onSaveCategory = onSaveCategory
)
}
}
@@ -578,7 +581,8 @@ fun PreviewAddExpenseDisabled() {
LocalDate.parse("2020-01-15"),
Currencies.entries.random().name
),
categories = categoriesToPreview
categories = categoriesToPreview,
{}
)
}
@@ -622,7 +626,8 @@ fun PreviewAddExpenseEnabled() {
LocalDate.parse("2020-01-11"),
Currencies.entries.random().name
),
categories = categoriesToPreview
categories = categoriesToPreview,
{}
)
}

View File

@@ -13,22 +13,26 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
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.string
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
import com.composables.icons.materialsymbols.outlined.R
@Composable
@@ -37,22 +41,41 @@ fun CategorySelectionDialog(
onCategorySelected: (Category) -> Unit,
selected: Category?,
categories: List<Category>,
onSaveCategory: (Category) -> Unit
) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val listState = rememberLazyListState()
var showAddCategoryDialog by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = {
onDismissRequest = onDismiss,
title = { Text(stringResource(string.pick_category)) },
text = {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if(selected != null) {
listState.animateScrollToItem(categories.indexOfFirst { it == selected })
}
// focusRequester.requestFocus()
}
Column {
var search by remember { mutableStateOf("") }
val filteredCategories = if (search.isBlank()) {
categories
} else {
categories.filter { category ->
category.name.lowercase().contains(search.lowercase())
}
}
LazyColumn(
modifier = Modifier.heightIn(max = 300.dp),
state = listState,
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(
count = categories.size,
key = { index -> categories[index].id }) { index ->
val category = categories[index]
count = filteredCategories.size,
key = { index -> filteredCategories[index].id }) { index ->
val category = filteredCategories[index]
Row(
modifier = Modifier
.fillMaxWidth()
@@ -83,24 +106,41 @@ fun CategorySelectionDialog(
.clickable {
showAddCategoryDialog = true
}
.padding(top = 15.dp),
.padding(bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
contentDescription = stringResource(string.category)
)
Text(
text = stringResource(string.add_new), modifier = Modifier.padding(start = 8.dp),
text = stringResource(string.add_new),
modifier = Modifier.padding(start = 8.dp),
)
}
SearchTextOutlined(
text = search,
onTextChange = { newText -> search = newText },
focusRequester = focusRequester
)
}
}, confirmButton = {})
},
confirmButton = {})
if (showAddCategoryDialog) {
AddCategoryDialog(onDismiss = {
showAddCategoryDialog = false
}, onSave = { category ->
expenseAndCategoryViewModel.save(category)
onSaveCategory(category)
showAddCategoryDialog = false
})
}
}
@AllPreviews
@Composable
fun PreviewCategorySelectionDialog() {
TripMoneyTheme {
CategorySelectionDialog(
{}, {},
categoriesToPreview.random(), categoriesToPreview, {})
}
}

View File

@@ -12,9 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -25,14 +23,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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies
import com.composables.icons.materialsymbols.outlined.R.drawable
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
@Composable
fun CurrencySelectionDialog(
@@ -57,20 +54,6 @@ fun CurrencySelectionDialog(
}
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
OutlinedTextField(
value = search,
onValueChange = { newText ->
search = newText
},
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
Icon(
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
contentDescription = "search"
)
}
)
val filteredCurrencies = if (search.isBlank()) {
currencies
} else {
@@ -79,7 +62,12 @@ fun CurrencySelectionDialog(
}
}
LazyColumn(state = scrollState) {
LazyColumn(
state = scrollState,
modifier = Modifier
.weight(1f)
.padding(bottom = 10.dp)
) {
items(
count = filteredCurrencies.size,
key = { index -> filteredCurrencies[index] }
@@ -104,6 +92,8 @@ fun CurrencySelectionDialog(
}
}
}
SearchTextOutlined(
text = search, onTextChange = { newText -> search = newText })
}
},
confirmButton = {},

View File

@@ -1,7 +1,5 @@
package cc.n0th1ng.tripmoney.screens.listexpense
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker
@@ -21,8 +19,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import java.time.Instant
@@ -31,7 +28,6 @@ import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateRangePicker(
@@ -75,7 +71,6 @@ fun DateRangePicker(
}
}
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DatePicker(
@@ -117,7 +112,6 @@ fun DatePicker(
}
}
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePicker(
@@ -147,7 +141,6 @@ fun TimePicker(
}
@Composable
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
fun DateTimePicker(
dateTime: LocalDateTime = LocalDateTime.now(),
@@ -181,15 +174,12 @@ fun DateTimePicker(
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDateTime.toEpochMilli(): Long =
this.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.toEpochMilli(): Long =
this.atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli()
this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun DatePickerPreview() {

View File

@@ -0,0 +1,155 @@
package cc.n0th1ng.tripmoney.screens.listexpense
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.n0th1ng.tripmoney.Filter
import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.navigation.AmountTextField
import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.pretty
import java.time.LocalDate
@Composable
fun FilterDialog(
onDismiss: () -> Unit,
onSave: (Filter) -> Unit,
onClear: () -> Unit,
categories: List<Category>,
filter: Filter
) {
var filter by remember { mutableStateOf(filter) }
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onClear
) { Text(stringResource(R.string.clear)) }
},
confirmButton = {
Button(
enabled = true,
onClick = {
onSave(filter)
}) { Text(stringResource(R.string.save)) }
}, title = { Text("Filter") },
text = {
var showDatePicker by remember { mutableStateOf(false) }
var startDate by remember {
mutableStateOf(filter.startDate)
}
var endDate by remember {
mutableStateOf(filter.endDate)
}
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Categories")
FlowRow(
horizontalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier
.sizeIn(maxHeight = 200.dp)
.verticalScroll(rememberScrollState())
) {
categories.forEach {
FilterChip(selected = filter.categories.contains(it), onClick = {
filter = if (filter.categories.contains(it)) {
filter.without(it)
} else {
filter.with(it)
}
}, label = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(painterResource(it.icon.resource), contentDescription = null)
Text(text = it.name)
}
})
}
}
Button(
modifier = Modifier
.fillMaxWidth(1f),
shape = MaterialTheme.shapes.medium,
onClick = { showDatePicker = true }) {
val startDateFormatted = startDate.pretty()
val endDateFormatted = endDate.pretty()
Text(
text =
if(startDate == LocalDate.MIN && endDate == LocalDate.MAX) "Show all dates" else
"$startDateFormatted - $endDateFormatted",
fontSize = 17.sp
)
}
AmountTextField(label = "from", onValueChange = { newText ->
fromAmountString = newText
filter = filter.withStartAmount(newText.safeToDouble())
}, value = fromAmountString)
AmountTextField(label = "to", onValueChange = { newText ->
toAmountString = newText
filter = filter.withEndAmount(newText.safeToDouble())
}, value = toAmountString)
}
if (showDatePicker) {
DateRangePicker(
startDate = if(startDate == LocalDate.MIN) LocalDate.now() else startDate,
endDate = if(endDate == LocalDate.MAX) LocalDate.now() else endDate,
onDismiss = { showDatePicker = false },
onConfirm = { newStartDate, newEndDate ->
startDate = newStartDate
endDate = newEndDate
filter = filter.withStartDate(startDate).withEndDate(endDate)
showDatePicker = false
})
}
})
}
@AllPreviews
@Composable
fun PreviewFilterDialog() {
TripMoneyTheme {
FilterDialog(
onDismiss = {},
onSave = {},
categories = categoriesToPreview.plus(categoriesToPreview).plus(categoriesToPreview),
filter = Filter(),
onClear = {}
)
}
}
private fun String.safeToDouble(): Double {
if (this == "") return Double.MAX_VALUE
if (this.isEmpty()) return 0.0
return this.toDouble()
}

View File

@@ -1,8 +1,6 @@
package cc.n0th1ng.tripmoney.screens.listexpense
import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
@@ -85,7 +83,6 @@ import java.time.format.DateTimeFormatter
import kotlin.random.Random
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun ListExpenseScreen(
filter: Filter,
@@ -124,7 +121,6 @@ fun ListExpenseScreen(
@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun ListExpenseScreen(
currentTrip: Trip?,
@@ -286,7 +282,6 @@ fun ListExpenseScreen(
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
Row(
@@ -329,7 +324,6 @@ fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun SwipeToDeleteExpenseCard(
expenseDto: ExpenseDto,
@@ -422,7 +416,6 @@ fun DeleteConfirmationDialog(
}
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun ExpenseCard(
expenseDto: ExpenseDto,
@@ -523,7 +516,6 @@ fun ExpenseCard(
}
}
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewListExpenseScreen() {
@@ -550,7 +542,6 @@ fun PreviewListExpenseScreen() {
}
}
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewListExpenseScreenWithoutExpenses() {
@@ -577,7 +568,6 @@ fun PreviewListExpenseScreenWithoutExpenses() {
}
}
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun PreviewListExpenseScreenWithoutTrip() {
@@ -609,7 +599,6 @@ fun PreviewDeleteConfirmationDialog() {
}
@RequiresApi(Build.VERSION_CODES.O)
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseListItemUi> {
val sampleCategories = listOf(
Category(

View File

@@ -88,7 +88,6 @@ fun StatisticsScreen(navController: NavController) {
)
}
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun StatisticsScreen(
summaryPerCategoryList: List<SummaryPerCategory>,
@@ -124,7 +123,7 @@ fun StatisticsScreen(
modifier = Modifier.heightIn(max = 300.dp),
summaryPerCategoryList = summaryPerCategoryList
)
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList, onDayClicked = onDayClicked)
SummaryPerDayCard(modifier = Modifier.height(300.dp), summaryPerDayList = summaryPerDayList.sortedBy { it.day }, onDayClicked = onDayClicked)
}
}

View File

@@ -1,15 +1,15 @@
package cc.n0th1ng.tripmoney.utils
val colors: List<String> = listOf(
"#af1b3f",
"#083D77",
"#5998c5",
"#f7934c",
"#ec0b43",
"#87A330",
"#6F8AB7",
"#F26CA7",
"#5E4AE3",
"#0B7189",
"#2A7F62",
"#0B7189"
"#5998c5",
"#5E4AE3",
"#6F8AB7",
"#87A330",
"#F26CA7",
"#af1b3f",
"#ec0b43",
"#f7934c"
)

View File

@@ -1,11 +1,8 @@
package cc.n0th1ng.tripmoney.utils
import android.os.Build
import androidx.annotation.RequiresApi
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.pretty(): String {
return this.format(DateTimeFormatter.ofPattern("dd MMM yyyy"))
}

View File

@@ -14,5 +14,10 @@ enum class Icons(@DrawableRes val resource: Int) {
COFFEE(R.drawable.materialsymbols_ic_local_cafe_outlined),
GENERAL(R.drawable.materialsymbols_ic_shoppingmode_outlined),
ENTERTAINMENT(R.drawable.materialsymbols_ic_theaters_outlined),
LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined)
LAUNDRY(R.drawable.materialsymbols_ic_local_laundry_service_outlined),
INSURANCE(R.drawable.materialsymbols_ic_health_and_safety_outlined),
SIM_DATA(R.drawable.materialsymbols_ic_sim_card_outlined),
CAR_RENTAL(R.drawable.materialsymbols_ic_directions_car_outlined),
FUEL(R.drawable.materialsymbols_ic_local_gas_station_outlined),
TOURS(R.drawable.materialsymbols_ic_tour_outlined)
}

View File

@@ -0,0 +1,45 @@
package cc.n0th1ng.tripmoney.utils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import com.composables.icons.materialsymbols.outlined.R.drawable
@Composable
fun SearchTextOutlined(
modifier: Modifier = Modifier,
text: String,
onTextChange: (String) -> Unit,
focusRequester: FocusRequester = FocusRequester()
) {
OutlinedTextField(
value = text,
onValueChange = onTextChange,
modifier = modifier
.fillMaxWidth()
.focusRequester(focusRequester),
trailingIcon = {
if (text.isNotBlank()) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "close",
modifier = Modifier.clickable(true, onClick = { onTextChange("") })
)
} else {
Icon(
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
contentDescription = "search"
)
}
}
)
}

View File

@@ -98,7 +98,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
): Flow<List<ExpenseDto>> =
expenseRepo.getExpensesDto(tripId, search, filter)
@RequiresApi(Build.VERSION_CODES.O)
fun save(expense: Expense, trip: Trip, onComplete: (Int) -> Unit) {
viewModelScope.launch {
val rate = exchangeRateRepository.getRate(
@@ -133,7 +132,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}
}
@RequiresApi(Build.VERSION_CODES.O)
suspend fun generateCSVToFile(tripId: Int, file: File) {
file.writer().use { writer ->
CSVPrinter(
@@ -153,7 +151,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
return getExpensesDto(tripId, search, filter)
.map { expenses ->
@@ -164,14 +161,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryAmount(tripId: Int): Flow<Double> {
return getExpensesDto(tripId).map { list ->
list.sumOf { it.expense.amount * it.expense.rate }
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
val tripFlow = tripRepo.getTrip(tripId)
val expensesFlow = getExpensesDto(tripId)
@@ -194,7 +189,6 @@ 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)
@@ -220,14 +214,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun clearOldRates() {
viewModelScope.launch {
exchangeRateRepository.clearOldRates()
}
}
@RequiresApi(Build.VERSION_CODES.O)
sealed class ExpenseListItemUi {
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
data class Header(val date: LocalDate, val sum: Double, val currency: String) :