init
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package cc.n0th1ng.tripmoney.screens
|
||||
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
@@ -28,19 +27,17 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.Colors
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import cc.n0th1ng.tripmoney.utils.colors
|
||||
|
||||
@Composable
|
||||
fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
var icon by remember { mutableStateOf(Icons.entries[0]) }
|
||||
var color by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
var color by remember { mutableStateOf(colors[0]) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss, title = { Text("Add new category") }, text = {
|
||||
AlertDialogFill(
|
||||
@@ -48,7 +45,7 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
||||
name = newText
|
||||
},
|
||||
onIconChange = { newIcon -> icon = newIcon },
|
||||
onColorChange = {newColor -> color = newColor}
|
||||
onColorChange = { newColor -> color = newColor }
|
||||
)
|
||||
}, confirmButton = {
|
||||
Button(
|
||||
@@ -72,10 +69,14 @@ fun AddCategoryDialog(onDismiss: () -> Unit, onSave: (Category) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Unit, onColorChange: (String) -> Unit) {
|
||||
fun AlertDialogFill(
|
||||
onTextChange: (String) -> Unit,
|
||||
onIconChange: (Icons) -> Unit,
|
||||
onColorChange: (String) -> Unit
|
||||
) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
var iconId by remember { mutableIntStateOf(Icons.entries[0].resource) }
|
||||
var colorHex by remember { mutableStateOf(Colors.entries[0].hexString) }
|
||||
var colorHex by remember { mutableStateOf(colors[0]) }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -118,16 +119,16 @@ fun AlertDialogFill(onTextChange: (String) -> Unit, onIconChange: (Icons) -> Uni
|
||||
rememberScrollState()
|
||||
)
|
||||
) {
|
||||
Colors.entries.forEach { color ->
|
||||
colors.forEach { color ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
colorHex = color.hexString
|
||||
colorHex = color
|
||||
onColorChange(colorHex)
|
||||
})
|
||||
.size(30.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(Color(color.hexString.toColorInt()))
|
||||
.background(Color(color.toColorInt()))
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -81,13 +82,13 @@ fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
expenseDtoToEdit: ExpenseDto?,
|
||||
state: SheetState,
|
||||
// categories: List<Category> = emptyList()
|
||||
state: SheetState
|
||||
) {
|
||||
val tripViewModel: TripViewModel = hiltViewModel()
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||
// val currentTripId = 1
|
||||
val currentTrip = tripViewModel.getTrip(currentTripId)
|
||||
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||
if (categories.isEmpty()) {
|
||||
return
|
||||
@@ -103,7 +104,7 @@ fun AddExpenseBottomSheet(
|
||||
var showDateTimePicker by remember { mutableStateOf(false) }
|
||||
var currency by remember {
|
||||
mutableStateOf(
|
||||
expenseDtoToEdit?.expense?.currency ?: Currencies.PLN.name
|
||||
expenseDtoToEdit?.expense?.currency ?: currentTrip?.currency ?: Currencies.default().name
|
||||
)
|
||||
}
|
||||
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
|
||||
|
||||
@@ -104,7 +104,9 @@ fun DateTimePicker(
|
||||
|
||||
if (showDatePicker) {
|
||||
DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
|
||||
date = newDate
|
||||
date = newDate
|
||||
showDatePicker = false
|
||||
showTimePicker = true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -53,34 +55,58 @@ 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 androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import cc.n0th1ng.tripmoney.R.string
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
||||
import cc.n0th1ng.tripmoney.service.ExchangeService
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.utils.colors
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseDtoWithConvertedAmount
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val expensesWithConvertedFlow = expenseAndCategoryViewModel
|
||||
.getExpensesWithConvertedAmountsPaged(currentTrip)
|
||||
|
||||
ListExpenseScreen(
|
||||
expensesWithConvertedFlow = expensesWithConvertedFlow,
|
||||
onSaveExpense = { expenseAndCategoryViewModel.save(it) },
|
||||
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ListExpenseScreen() {
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
|
||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||
val expenses = expenseAndCategoryViewModel.getExpenses(currentTrip).collectAsLazyPagingItems()
|
||||
fun ListExpenseScreen(
|
||||
expensesWithConvertedFlow: Flow<PagingData<ExpenseDtoWithConvertedAmount>>,
|
||||
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit
|
||||
) {
|
||||
val expensesWithConverted = expensesWithConvertedFlow.collectAsLazyPagingItems()
|
||||
val listState = rememberLazyListState()
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
var expenseDtoToEdit: ExpenseDto? = null
|
||||
val sumMap = remember { mutableStateMapOf<LocalDate, Double>() }
|
||||
|
||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
@@ -90,30 +116,50 @@ fun ListExpenseScreen() {
|
||||
)
|
||||
})
|
||||
{
|
||||
LaunchedEffect(expensesWithConverted.itemSnapshotList.items) {
|
||||
val items = expensesWithConverted.itemSnapshotList.items
|
||||
val newSums = items
|
||||
.groupBy { LocalDateTime.parse(it.expenseDto.expense.datetime).toLocalDate() }
|
||||
.mapValues { (_, expensesForDay) ->
|
||||
expensesForDay.sumOf { it.convertedAmount }
|
||||
}
|
||||
sumMap.clear()
|
||||
sumMap.putAll(newSums)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
state = listState
|
||||
) {
|
||||
items(
|
||||
count = expenses.itemCount,
|
||||
key = { index -> expenses[index]?.expense?.id ?: index }
|
||||
count = expensesWithConverted.itemCount,
|
||||
key = { index -> expensesWithConverted[index]?.expenseDto?.expense?.id ?: index }
|
||||
) { index ->
|
||||
val expenseDto = expenses[index]
|
||||
if (expenseDto != null) {
|
||||
val previousExpense = expenses.itemSnapshotList.items.getOrNull(index - 1)
|
||||
val expenseDtoWithConverted = expensesWithConverted[index]
|
||||
val expenseDto = expenseDtoWithConverted?.expenseDto
|
||||
if (expenseDtoWithConverted != null && expenseDto != null) {
|
||||
val previousExpense =
|
||||
expensesWithConverted.itemSnapshotList.items.getOrNull(index - 1)?.expenseDto
|
||||
val showDayDivider =
|
||||
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
||||
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
||||
.toLocalDate()
|
||||
Spacer(Modifier.height(5.dp))
|
||||
Spacer(Modifier
|
||||
.height(5.dp)
|
||||
.background(MaterialTheme.colorScheme.onBackground))
|
||||
if (showDayDivider) {
|
||||
CustomDivider(expenseDto)
|
||||
CustomDivider(
|
||||
expenseDto,
|
||||
sumMap.getOrDefault(
|
||||
LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate(), 0.00
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(5.dp))
|
||||
SwipeToDeleteExpenseCard(
|
||||
expenseDto = expenseDto,
|
||||
onDelete = { expense -> expenseAndCategoryViewModel.delete(expense) },
|
||||
expenseDtoWithConverted = expenseDtoWithConverted,
|
||||
onDelete = { expense -> onDeleteExpense(expense) },
|
||||
onClick = { expenseDto ->
|
||||
expenseDtoToEdit = expenseDto
|
||||
showBottomSheet = true
|
||||
@@ -125,7 +171,7 @@ fun ListExpenseScreen() {
|
||||
if (showBottomSheet) {
|
||||
AddExpenseBottomSheet(
|
||||
onSave = { expense ->
|
||||
expenseAndCategoryViewModel.save(expense)
|
||||
onSaveExpense(expense)
|
||||
showBottomSheet = false
|
||||
expenseDtoToEdit = null
|
||||
},
|
||||
@@ -140,9 +186,10 @@ fun ListExpenseScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun CustomDivider(expenseDto: ExpenseDto) {
|
||||
fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Absolute.Center,
|
||||
@@ -153,16 +200,32 @@ fun CustomDivider(expenseDto: ExpenseDto) {
|
||||
LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||
DateTimeFormatter.ofPattern("dd EEEE")
|
||||
).toString(),
|
||||
modifier = Modifier.background(Color.White.copy(alpha = 0f))
|
||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.Absolute.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HorizontalDivider(modifier = Modifier.weight(2f))
|
||||
Text(
|
||||
"%.2f %s".format(sum, expenseDto.trip.currency),
|
||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun SwipeToDeleteExpenseCard(
|
||||
expenseDto: ExpenseDto,
|
||||
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
|
||||
onDelete: (Expense) -> Unit,
|
||||
onClick: (ExpenseDto) -> Unit
|
||||
) {
|
||||
@@ -186,7 +249,7 @@ fun SwipeToDeleteExpenseCard(
|
||||
onConfirm = {
|
||||
showDialog = false
|
||||
dismissed = true
|
||||
onDelete(expenseDto.expense)
|
||||
onDelete(expenseDtoWithConverted.expenseDto.expense)
|
||||
},
|
||||
onCancel = { showDialog = false }
|
||||
)
|
||||
@@ -209,7 +272,7 @@ fun SwipeToDeleteExpenseCard(
|
||||
}
|
||||
}
|
||||
) {
|
||||
ExpenseCard(expenseDto, onClick = onClick)
|
||||
ExpenseCard(expenseDtoWithConverted, onClick = onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,15 +289,15 @@ fun DeleteConfirmationDialog(
|
||||
Column(
|
||||
Modifier
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surface,
|
||||
MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(string.delete_confirmation),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
@@ -244,6 +307,8 @@ fun DeleteConfirmationDialog(
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(string.cancel),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier
|
||||
.padding(end = 24.dp)
|
||||
.clickable { onCancel() }
|
||||
@@ -251,7 +316,7 @@ fun DeleteConfirmationDialog(
|
||||
Text(
|
||||
text = stringResource(string.delete),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.clickable { onConfirm() }
|
||||
)
|
||||
}
|
||||
@@ -261,8 +326,14 @@ fun DeleteConfirmationDialog(
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
fun ExpenseCard(
|
||||
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
|
||||
onClick: (ExpenseDto) -> Unit
|
||||
) {
|
||||
val expenseDto = expenseDtoWithConverted.expenseDto
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors()
|
||||
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.height(70.dp)
|
||||
@@ -299,14 +370,14 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
{
|
||||
Text(
|
||||
text = expenseDto.category.name,
|
||||
fontWeight = FontWeight.Bold,
|
||||
lineHeight = 5.sp
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(0.dp),
|
||||
text = expenseDto.expense.note,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 5.sp
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
@@ -314,31 +385,132 @@ fun ExpenseCard(expenseDto: ExpenseDto, onClick: (ExpenseDto) -> Unit) {
|
||||
text = LocalDateTime.parse(expenseDto.expense.datetime).format(
|
||||
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
||||
),
|
||||
fontSize = 12.sp,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = "- %.2f ${expenseDto.expense.currency}".format(expenseDto.expense.amount),
|
||||
fontWeight = FontWeight.Bold
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
|
||||
|
||||
)
|
||||
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val amount by
|
||||
expenseAndCategoryViewModel.convertAmount(
|
||||
amount = expenseDto.expense.amount,
|
||||
base = Currencies.valueOf(expenseDto.expense.currency),
|
||||
target = Currencies.valueOf(expenseDto.trip.currency),
|
||||
date = LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate()
|
||||
).collectAsState(initial = 0.0)
|
||||
Text(
|
||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(amount),
|
||||
fontSize = 12.sp
|
||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDtoWithConverted.convertedAmount),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewListExpenseScreen() {
|
||||
TripMoneyTheme() {
|
||||
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
|
||||
ListExpenseScreen(
|
||||
expensesWithConvertedFlow = MutableStateFlow(pagingData),
|
||||
onSaveExpense = {},
|
||||
onDeleteExpense = {}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@AllPreviews
|
||||
@Composable
|
||||
fun PreviewDeleteConfirmationDialog() {
|
||||
TripMoneyTheme() {
|
||||
DeleteConfirmationDialog(
|
||||
onConfirm = {},
|
||||
onCancel = {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseDtoWithConvertedAmount> {
|
||||
val sampleCategories = listOf(
|
||||
Category(
|
||||
name = "Hotel",
|
||||
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||
color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Jedzenie",
|
||||
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||
color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Transport",
|
||||
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||
color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Rozrywka",
|
||||
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||
color = colors.random()
|
||||
),
|
||||
Category(
|
||||
name = "Zakupy",
|
||||
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||
color = colors.random()
|
||||
),
|
||||
)
|
||||
|
||||
val trip = Trip(
|
||||
id = 1,
|
||||
name = "Vacation",
|
||||
currency = "USD",
|
||||
startDate = "2026-01-01"
|
||||
)
|
||||
|
||||
val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
|
||||
val endLong = LocalDateTime.now().toEpochMilli()
|
||||
|
||||
val result: MutableList<ExpenseDtoWithConvertedAmount> = mutableListOf()
|
||||
for (i in 0..15) {
|
||||
val category = sampleCategories.random()
|
||||
val datetime = if (i > 4) {
|
||||
LocalDateTime.ofEpochSecond(
|
||||
Random.nextLong(startLong, endLong),
|
||||
0,
|
||||
ZoneOffset.UTC
|
||||
).toString()
|
||||
} else LocalDateTime.now().toString()
|
||||
|
||||
val expense = Expense(
|
||||
id = i,
|
||||
categoryId = category.id,
|
||||
tripId = 1,
|
||||
amount = Random.nextDouble(0.1, 300.0),
|
||||
currency = Currencies.entries.random().name,
|
||||
note = if (i % 3 == 0) "Some note" else "",
|
||||
datetime = datetime
|
||||
)
|
||||
val expenseDto = ExpenseDto(
|
||||
expense = expense,
|
||||
category = category,
|
||||
trip = trip
|
||||
)
|
||||
result.add(
|
||||
ExpenseDtoWithConvertedAmount(
|
||||
expenseDto,
|
||||
convertedAmount = if (Random.nextBoolean()) Random.nextDouble(
|
||||
0.1,
|
||||
300.0
|
||||
) else expense.amount
|
||||
)
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import cc.n0th1ng.tripmoney.R.*
|
||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@@ -40,8 +42,9 @@ import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
fun SettingsScreen() {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTheme by settingsViewModel.theme.collectAsState()
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
|
||||
var showThemeDialog by remember { mutableStateOf(false) }
|
||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -49,7 +52,7 @@ fun SettingsScreen() {
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Card {
|
||||
SettingsListItem(onClick = { showDialog = true }, stringResource(string.theme)) {
|
||||
SettingsListItem(onClick = { showThemeDialog = true }, stringResource(string.theme)) {
|
||||
Text(
|
||||
if (isSystemInDarkTheme()) stringResource(string.dark_theme) else stringResource(
|
||||
string.light_theme
|
||||
@@ -58,16 +61,33 @@ fun SettingsScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
Card {
|
||||
SettingsListItem(
|
||||
onClick = { showCurrencyDialog = true },
|
||||
stringResource(string.default_currency)
|
||||
) {
|
||||
Text(currentDefaultCurrency.name)
|
||||
}
|
||||
}
|
||||
|
||||
if (showThemeDialog) {
|
||||
ThemeSelectionDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onDismiss = { showThemeDialog = false },
|
||||
onThemeSelected = { theme ->
|
||||
settingsViewModel.setTheme(theme)
|
||||
showDialog = false
|
||||
showThemeDialog = false
|
||||
},
|
||||
selected = currentTheme
|
||||
)
|
||||
}
|
||||
|
||||
if (showCurrencyDialog) {
|
||||
CurrencySelectionDialog(onDismiss = {showCurrencyDialog = false}, onCurrencySelected = {
|
||||
currencyString ->
|
||||
settingsViewModel.setDefaultCurrency(Currencies.valueOf(currencyString))
|
||||
showCurrencyDialog = false
|
||||
}, currentDefaultCurrency.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,150 @@
|
||||
package cc.n0th1ng.tripmoney.screens.statistics
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.entity.Category
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import cc.n0th1ng.tripmoney.utils.colors
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun StatisticsScreen() {
|
||||
Text("TODO")
|
||||
}
|
||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
||||
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTrip)
|
||||
.collectAsState(emptyList())
|
||||
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
SummaryPerCategoryCard(summaryPerCategoryList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(15.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
// Text(text = "Summary", fontWeight = FontWeight.Bold, fontSize = 25.sp)
|
||||
summaryPerCategoryList.forEach {
|
||||
CategoryCard(
|
||||
summaryPerCategory = it, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCategory) {
|
||||
Column(modifier = modifier) {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Icon(
|
||||
painter = painterResource(summaryPerCategory.category.icon.resource),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(MaterialTheme.typography.bodyLarge.fontSize.value.dp),
|
||||
tint = Color(summaryPerCategory.category.color.toColorInt())
|
||||
)
|
||||
Text("%s".format(summaryPerCategory.category.name, (summaryPerCategory.percent * 100).toInt()),
|
||||
style = MaterialTheme.typography.bodyLarge, color = Color(summaryPerCategory.category.color.toColorInt()))
|
||||
}
|
||||
|
||||
Text("%.2f ${summaryPerCategory.currency}".format(summaryPerCategory.amount),
|
||||
style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(5.dp), verticalAlignment = Alignment.CenterVertically){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(40.dp)
|
||||
.fillMaxWidth(0.12f + (0.90f - 0.12f) * summaryPerCategory.percent)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().padding(11.dp)) {
|
||||
Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
|
||||
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onPrimary)
|
||||
|
||||
}
|
||||
}
|
||||
// Text("%d%%".format((summaryPerCategory.percent * 100).toInt()),
|
||||
// style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun previewLight() {
|
||||
TripMoneyTheme {
|
||||
SummaryPerCategoryCard(summaryPerCategoryList)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun previewDark() {
|
||||
TripMoneyTheme(darkTheme = true) {
|
||||
SummaryPerCategoryCard(summaryPerCategoryList)
|
||||
}
|
||||
}
|
||||
|
||||
val categories = listOf(
|
||||
Category(name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()),
|
||||
Category(name = "Transport", icon = Icons.FLIGHT, color = colors.random()),
|
||||
Category(name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()),
|
||||
Category(name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()),
|
||||
Category(name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()),
|
||||
Category(name = "Zakupy2", icon = Icons.GROCERIES, color = colors.random()),
|
||||
Category(name = "Zakupy3", icon = Icons.GROCERIES, color = colors.random())
|
||||
)
|
||||
|
||||
val summaryPerCategoryList = listOf(
|
||||
SummaryPerCategory(categories[0], 50.0, 1f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[1], 120.0, 0.3f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[2], 80.0, 0.2f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[3], 50.0, 0.1f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[4], 120.0, 0.3f, Currencies.PLN),
|
||||
SummaryPerCategory(categories[5], 50.0, 0.0001f, Currencies.PLN),
|
||||
)
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -37,12 +38,15 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
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 cc.n0th1ng.tripmoney.R
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.screens.addexpense.CurrencyButton
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||
import cc.n0th1ng.tripmoney.screens.listexpense.DatePicker
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import io.ktor.http.hostIsIp
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -62,9 +66,11 @@ fun AddTripBottomSheet(
|
||||
LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString())
|
||||
)
|
||||
}
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val defaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
|
||||
var showCurrencyDialog by remember { mutableStateOf(false) }
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var currency by remember { mutableStateOf(tripToEdit?.currency ?: Currencies.default().name) }
|
||||
var currency by remember { mutableStateOf(tripToEdit?.currency ?: defaultCurrency.name) }
|
||||
var enableSave by remember { mutableStateOf(tripToEdit != null) }
|
||||
|
||||
ModalBottomSheet(
|
||||
|
||||
@@ -182,6 +182,13 @@ fun TripCard(
|
||||
) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
ElevatedCard(
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
),
|
||||
modifier = Modifier
|
||||
.height(100.dp)
|
||||
.combinedClickable(enabled = true, onLongClick = {
|
||||
|
||||
Reference in New Issue
Block a user