init #48
@@ -20,6 +20,7 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
||||||
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
||||||
import cc.n0th1ng.tripmoney.navigation.Screens
|
import cc.n0th1ng.tripmoney.navigation.Screens
|
||||||
@@ -59,7 +60,7 @@ fun NavigationDrawer() {
|
|||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip = tripViewModel.getTrip(currentTripId)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val navBackStack by navController.currentBackStackEntryAsState()
|
val navBackStack by navController.currentBackStackEntryAsState()
|
||||||
val current = navBackStack?.destination?.route
|
val current = navBackStack?.destination?.route
|
||||||
|
|||||||
41
app/src/main/java/cc/n0th1ng/tripmoney/data/Converters.kt
Normal file
41
app/src/main/java/cc/n0th1ng/tripmoney/data/Converters.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package cc.n0th1ng.tripmoney.data
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalDatetime(value: LocalDateTime): Long {
|
||||||
|
return value
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalDateTime(value: Long): LocalDateTime {
|
||||||
|
return Instant.ofEpochMilli(value)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toLocalDateTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalDate(value: LocalDate): Long {
|
||||||
|
return value.toEpochDay()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalDate(value: Long): LocalDate {
|
||||||
|
return LocalDate.ofEpochDay(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||||
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
|
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
|
||||||
@@ -17,6 +18,7 @@ import cc.n0th1ng.tripmoney.data.entity.Expense
|
|||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.utils.Icons
|
import cc.n0th1ng.tripmoney.utils.Icons
|
||||||
import cc.n0th1ng.tripmoney.utils.colors
|
import cc.n0th1ng.tripmoney.utils.colors
|
||||||
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -25,11 +27,13 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1)
|
@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
abstract class TripDatabase : RoomDatabase() {
|
abstract class TripDatabase : RoomDatabase() {
|
||||||
abstract fun tripDao(): TripDao
|
abstract fun tripDao(): TripDao
|
||||||
abstract fun expenseDao(): ExpenseDao
|
abstract fun expenseDao(): ExpenseDao
|
||||||
@@ -46,7 +50,8 @@ object DatabaseModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideTripDatabase(
|
fun provideTripDatabase(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context,
|
||||||
|
// expenseAndCategoryViewModel: ExpenseAndCategoryViewModel
|
||||||
): TripDatabase {
|
): TripDatabase {
|
||||||
|
|
||||||
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
|
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
|
||||||
@@ -94,9 +99,9 @@ private class DatabasePrepopulator(
|
|||||||
) {
|
) {
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
suspend fun prepopulate() {
|
suspend fun prepopulate() {
|
||||||
tripDao.insert(Trip(name = "Włochy", startDate = "2026-03-01", currency = "PLN"))
|
tripDao.insert(Trip(name = "Włochy", startDate = LocalDate.parse("2026-03-01"), currency = "PLN"))
|
||||||
tripDao.insert(Trip(name = "Szwajcaria", startDate = "2025-03-01", currency = "EUR"))
|
tripDao.insert(Trip(name = "Szwajcaria", startDate =LocalDate.parse("2025-03-01"), currency = "EUR"))
|
||||||
tripDao.insert(Trip(name = "Portugalia", startDate = "2025-03-01", currency = "USD"))
|
tripDao.insert(Trip(name = "Portugalia", startDate = LocalDate.parse("2025-03-01"), currency = "USD"))
|
||||||
categoryDao.insert(Category(name = "Accomodation", icon = Icons.HOTEL, color = colors.random()))
|
categoryDao.insert(Category(name = "Accomodation", icon = Icons.HOTEL, color = colors.random()))
|
||||||
categoryDao.insert(Category(name = "Transport", icon = Icons.TRANSPORT, color = colors.random()))
|
categoryDao.insert(Category(name = "Transport", icon = Icons.TRANSPORT, color = colors.random()))
|
||||||
categoryDao.insert(Category(name = "Flight", icon = Icons.FLIGHT, color = colors.random()))
|
categoryDao.insert(Category(name = "Flight", icon = Icons.FLIGHT, color = colors.random()))
|
||||||
@@ -113,7 +118,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 120.50,
|
amount = 120.50,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Hotel overnight",
|
note = "Hotel overnight",
|
||||||
datetime = now.minusDays(10).toString(),
|
datetime = now.minusDays(10),
|
||||||
categoryId = 1,
|
categoryId = 1,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -123,7 +128,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 45.75,
|
amount = 45.75,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Dinner",
|
note = "Dinner",
|
||||||
datetime = now.minusDays(9).toString(),
|
datetime = now.minusDays(9),
|
||||||
categoryId = 2,
|
categoryId = 2,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -133,7 +138,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 15.20,
|
amount = 15.20,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Bus ticket",
|
note = "Bus ticket",
|
||||||
datetime = now.minusDays(8).toString(),
|
datetime = now.minusDays(8),
|
||||||
categoryId = 3,
|
categoryId = 3,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -143,7 +148,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 89.99,
|
amount = 89.99,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Concert tickets",
|
note = "Concert tickets",
|
||||||
datetime = now.minusDays(7).toString(),
|
datetime = now.minusDays(7),
|
||||||
categoryId = 4,
|
categoryId = 4,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -153,7 +158,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 32.50,
|
amount = 32.50,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Souvenirs",
|
note = "Souvenirs",
|
||||||
datetime = now.minusDays(6).toString(),
|
datetime = now.minusDays(6),
|
||||||
categoryId = 5,
|
categoryId = 5,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -163,7 +168,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 180.00,
|
amount = 180.00,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Hotel 3 nights",
|
note = "Hotel 3 nights",
|
||||||
datetime = now.minusDays(5).toString(),
|
datetime = now.minusDays(5),
|
||||||
categoryId = 1,
|
categoryId = 1,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -173,7 +178,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 67.30,
|
amount = 67.30,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Lunch",
|
note = "Lunch",
|
||||||
datetime = now.minusDays(4).toString(),
|
datetime = now.minusDays(4),
|
||||||
categoryId = 2,
|
categoryId = 2,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -183,7 +188,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 22.00,
|
amount = 22.00,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Train ticket",
|
note = "Train ticket",
|
||||||
datetime = now.minusDays(3).toString(),
|
datetime = now.minusDays(3),
|
||||||
categoryId = 3,
|
categoryId = 3,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -193,7 +198,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 55.00,
|
amount = 55.00,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Museum entry",
|
note = "Museum entry",
|
||||||
datetime = now.minusDays(2).toString(),
|
datetime = now.minusDays(2),
|
||||||
categoryId = 4,
|
categoryId = 4,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -203,7 +208,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 12.99,
|
amount = 12.99,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Snacks",
|
note = "Snacks",
|
||||||
datetime = now.minusDays(1).toString(),
|
datetime = now.minusDays(1),
|
||||||
categoryId = 2,
|
categoryId = 2,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -213,7 +218,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 210.00,
|
amount = 210.00,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Hotel 5 nights",
|
note = "Hotel 5 nights",
|
||||||
datetime = now.toString(),
|
datetime = now,
|
||||||
categoryId = 1,
|
categoryId = 1,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -223,7 +228,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 95.50,
|
amount = 95.50,
|
||||||
currency = "EUR",
|
currency = "EUR",
|
||||||
note = "Dinner for two",
|
note = "Dinner for two",
|
||||||
datetime = now.minusHours(12).toString(),
|
datetime = now.minusHours(12),
|
||||||
categoryId = 2,
|
categoryId = 2,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -233,7 +238,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 30.00,
|
amount = 30.00,
|
||||||
currency = "EUR",
|
currency = "EUR",
|
||||||
note = "Taxi",
|
note = "Taxi",
|
||||||
datetime = now.minusHours(6).toString(),
|
datetime = now.minusHours(6),
|
||||||
categoryId = 3,
|
categoryId = 3,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -243,7 +248,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 40.00,
|
amount = 40.00,
|
||||||
currency = "USD",
|
currency = "USD",
|
||||||
note = "Gifts",
|
note = "Gifts",
|
||||||
datetime = now.minusHours(3).toString(),
|
datetime = now.minusHours(3),
|
||||||
categoryId = 5,
|
categoryId = 5,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
@@ -253,7 +258,7 @@ private class DatabasePrepopulator(
|
|||||||
amount = 75.00,
|
amount = 75.00,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "Sightseeing tour",
|
note = "Sightseeing tour",
|
||||||
datetime = now.minusHours(1).toString(),
|
datetime = now.minusHours(1),
|
||||||
categoryId = 4,
|
categoryId = 4,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface ExpenseDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM expense WHERE trip_id = :tripId
|
SELECT * FROM expense WHERE trip_id = :tripId
|
||||||
ORDER BY DATETIME(expense.datetime) DESC
|
ORDER BY expense.datetime DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun expenseDtoPaged(tripId: Int): PagingSource<Int, ExpenseDto>
|
fun expenseDtoPaged(tripId: Int): PagingSource<Int, ExpenseDto>
|
||||||
@@ -28,33 +28,11 @@ interface ExpenseDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM expense WHERE trip_id = :tripId
|
SELECT * FROM expense WHERE trip_id = :tripId
|
||||||
ORDER BY DATETIME(expense.datetime) DESC
|
ORDER BY expense.datetime DESC
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun expenseDto(tripId: Int): Flow<List<ExpenseDto>>
|
fun expenseDto(tripId: Int): Flow<List<ExpenseDto>>
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(expense: Expense)
|
suspend fun delete(expense: Expense)
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
c.id as categoryId,
|
|
||||||
c.name as categoryName,
|
|
||||||
c.icon as icon,
|
|
||||||
c.color as color,
|
|
||||||
SUM(e.amount) as amount,
|
|
||||||
e.currency as currency
|
|
||||||
FROM
|
|
||||||
expense e
|
|
||||||
JOIN
|
|
||||||
category c ON e.category_id = c.id
|
|
||||||
WHERE
|
|
||||||
e.trip_id = :tripId
|
|
||||||
GROUP BY
|
|
||||||
c.id, c.name, c.icon, c.color, e.currency
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
fun summaryPerCategoryRaw(tripId: Int): Flow<List<SummaryPerCategoryRaw>>
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.room.Insert
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TripDao {
|
interface TripDao {
|
||||||
@@ -27,5 +28,5 @@ interface TripDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM trip where trip.id = :tripId"
|
"SELECT * FROM trip where trip.id = :tripId"
|
||||||
)
|
)
|
||||||
fun trip(tripId: Int): Trip?
|
fun trip(tripId: Int): Flow<Trip?>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Embedded
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Entity(tableName = "expense")
|
@Entity(tableName = "expense")
|
||||||
data class Expense(
|
data class Expense(
|
||||||
@@ -12,10 +13,15 @@ data class Expense(
|
|||||||
@ColumnInfo("amount") val amount: Double,
|
@ColumnInfo("amount") val amount: Double,
|
||||||
@ColumnInfo("currency") val currency: String,
|
@ColumnInfo("currency") val currency: String,
|
||||||
@ColumnInfo("note") val note: String,
|
@ColumnInfo("note") val note: String,
|
||||||
@ColumnInfo("datetime") val datetime: String,
|
@ColumnInfo("datetime") val datetime: LocalDateTime,
|
||||||
@ColumnInfo("category_id") val categoryId: Int,
|
@ColumnInfo("category_id") val categoryId: Int,
|
||||||
@ColumnInfo("trip_id") val tripId: Int
|
@ColumnInfo("trip_id") val tripId: Int,
|
||||||
)
|
@ColumnInfo("rate") val rate: Double = 1.0
|
||||||
|
) {
|
||||||
|
fun convertedAmount(): Double {
|
||||||
|
return this.amount * this.rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ExpenseDto(
|
data class ExpenseDto(
|
||||||
@Embedded val expense: Expense,
|
@Embedded val expense: Expense,
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.entity
|
package cc.n0th1ng.tripmoney.data.entity
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
@Entity(tableName = "trip")
|
@Entity(tableName = "trip")
|
||||||
data class Trip(
|
data class Trip(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
@ColumnInfo("name") val name: String,
|
@ColumnInfo("name") val name: String,
|
||||||
@ColumnInfo("start_date") val startDate: String,
|
@ColumnInfo("start_date") val startDate: LocalDate,
|
||||||
@ColumnInfo("currency") val currency: String
|
@ColumnInfo("currency") val currency: String
|
||||||
)
|
){
|
||||||
|
companion object {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
val DUMMY = Trip(-1, "dummy", LocalDate.now(), Currencies.default().name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ class ExchangeRateRepository @Inject constructor(
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double {
|
suspend fun getRate(base: Currencies, target: Currencies, date: LocalDate): Double {
|
||||||
|
if(base == target) return 1.0
|
||||||
val id = ExchangeRate.buildKey(base.name, target.name, date.toString())
|
val id = ExchangeRate.buildKey(base.name, target.name, date.toString())
|
||||||
val cachedRate = exchangeRateDao.getById(id)
|
val cachedRate = exchangeRateDao.getById(id)
|
||||||
return if (cachedRate != null) {
|
return if (cachedRate != null) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.repository
|
package cc.n0th1ng.tripmoney.data.repository
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
@@ -8,10 +11,16 @@ import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
|||||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw
|
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ExpenseRepository @Inject constructor(private val expenseDao: ExpenseDao) {
|
class ExpenseRepository @Inject constructor(
|
||||||
|
private val expenseDao: ExpenseDao,
|
||||||
|
private val exchangeRateRepository: ExchangeRateRepository
|
||||||
|
) {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun save(expense: Expense) {
|
suspend fun save(expense: Expense) {
|
||||||
@@ -23,18 +32,30 @@ class ExpenseRepository @Inject constructor(private val expenseDao: ExpenseDao)
|
|||||||
expenseDao.delete(expense)
|
expenseDao.delete(expense)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesPaged(tripId: Int): Flow<PagingData<ExpenseDto>> {
|
fun getExpensesDtoPaged(tripId: Int): Flow<PagingData<ExpenseDto>> {
|
||||||
return Pager(
|
return Pager(
|
||||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
||||||
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId) }
|
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId) }
|
||||||
).flow
|
).flow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpenses(tripId: Int): Flow<List<ExpenseDto>> {
|
fun getExpensesDto(tripId: Int): Flow<List<ExpenseDto>> {
|
||||||
return expenseDao.expenseDto(tripId)
|
return expenseDao.expenseDto(tripId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategoryRaw>> {
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
return expenseDao.summaryPerCategoryRaw(tripId)
|
suspend fun recalculateTripExpenses(tripId: Int) {
|
||||||
|
val expenses = getExpensesDto(tripId).first()
|
||||||
|
expenses.forEach { expenseDto ->
|
||||||
|
val newRate = exchangeRateRepository.getRate(
|
||||||
|
Currencies.valueOf(expenseDto.expense.currency),
|
||||||
|
Currencies.valueOf(expenseDto.trip.currency),
|
||||||
|
expenseDto.expense.datetime.toLocalDate()
|
||||||
|
)
|
||||||
|
save(
|
||||||
|
expenseDto.expense.copy(rate = newRate)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,26 @@
|
|||||||
package cc.n0th1ng.tripmoney.data.repository
|
package cc.n0th1ng.tripmoney.data.repository
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
import cc.n0th1ng.tripmoney.data.dao.TripDao
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TripRepository @Inject constructor(private val tripDao: TripDao) {
|
class TripRepository @Inject constructor(
|
||||||
|
private val tripDao: TripDao,
|
||||||
|
private val expenseRepository: ExpenseRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun save(trip: Trip) {
|
suspend fun save(trip: Trip) {
|
||||||
|
expenseRepository.recalculateTripExpenses(trip.id)
|
||||||
tripDao.insert(trip)
|
tripDao.insert(trip)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +31,7 @@ class TripRepository @Inject constructor(private val tripDao: TripDao) {
|
|||||||
).flow
|
).flow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTrip(tripId: Int): Trip? {
|
fun getTrip(tripId: Int): Flow<Trip?> {
|
||||||
return tripDao.trip(tripId)
|
return tripDao.trip(tripId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
package cc.n0th1ng.tripmoney.screens.addexpense
|
package cc.n0th1ng.tripmoney.screens.addexpense
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.drawable.PaintDrawable
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.PressInteraction
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonColors
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.DividerDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SheetState
|
import androidx.compose.material3.SheetState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableDoubleStateOf
|
import androidx.compose.runtime.mutableDoubleStateOf
|
||||||
@@ -50,17 +45,13 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
import androidx.compose.ui.modifier.modifierLocalMapOf
|
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.sensitiveContent
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
@@ -83,10 +74,12 @@ import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
|||||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.collections.listOf
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@@ -101,14 +94,15 @@ fun AddExpenseBottomSheet(
|
|||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip = tripViewModel.getTrip(currentTripId)!!
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
|
|
||||||
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||||
AddExpenseBottomSheet(
|
AddExpenseBottomSheet(
|
||||||
onSave = onSave,
|
onSave = onSave,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
expenseDtoToEdit = expenseDtoToEdit,
|
expenseDtoToEdit = expenseDtoToEdit,
|
||||||
state = state,
|
state = state,
|
||||||
currentTrip = currentTrip,
|
currentTrip = currentTrip!!,
|
||||||
categories = categories
|
categories = categories
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -148,9 +142,7 @@ fun AddExpenseBottomSheet(
|
|||||||
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
|
var category by remember { mutableStateOf(expenseDtoToEdit?.category ?: categories[0]) }
|
||||||
var datetime by remember {
|
var datetime by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
LocalDateTime.parse(
|
expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now()
|
||||||
expenseDtoToEdit?.expense?.datetime ?: LocalDateTime.now().toString()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
|
var note by remember { mutableStateOf(expenseDtoToEdit?.expense?.note ?: "") }
|
||||||
@@ -192,14 +184,16 @@ fun AddExpenseBottomSheet(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Column{
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = amount.ifEmpty { "0.00" },
|
text = amount.ifEmpty { "0.00" },
|
||||||
fontSize = 25.sp,
|
fontSize = 25.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if(amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format(equationResult) else "",
|
text = if (amount.contains(Regex("[+\\/*-]\\d+"))) "%.2f".format(
|
||||||
|
equationResult
|
||||||
|
) else "",
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -244,12 +238,11 @@ fun AddExpenseBottomSheet(
|
|||||||
NumberKeyboard(
|
NumberKeyboard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onOperatorClick = { operator ->
|
onOperatorClick = { operator ->
|
||||||
if(amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+\\/*-]\\d+"))) {
|
if (amount.isDoubleTwoDigitsOrEquation() && amount.contains(Regex("[+\\/*-]\\d+"))) {
|
||||||
amount = evaluate(amount).toString()
|
amount = evaluate(amount).toString()
|
||||||
// equationResult = 0.0
|
|
||||||
}
|
}
|
||||||
val newText = amount + operator
|
val newText = amount + operator
|
||||||
if(newText.isDoubleTwoDigitsOrEquation()) {
|
if (newText.isDoubleTwoDigitsOrEquation()) {
|
||||||
amount = newText
|
amount = newText
|
||||||
enableSave = false
|
enableSave = false
|
||||||
}
|
}
|
||||||
@@ -282,7 +275,7 @@ fun AddExpenseBottomSheet(
|
|||||||
amount = equationResult,
|
amount = equationResult,
|
||||||
currency = currency,
|
currency = currency,
|
||||||
note = note,
|
note = note,
|
||||||
datetime = datetime.toString(),
|
datetime = datetime,
|
||||||
categoryId = category.id,
|
categoryId = category.id,
|
||||||
tripId = currentTripId
|
tripId = currentTripId
|
||||||
)
|
)
|
||||||
@@ -330,7 +323,7 @@ fun AddExpenseBottomSheet(
|
|||||||
fun String.safeSubstring(start: Int, end: Int): String {
|
fun String.safeSubstring(start: Int, end: Int): String {
|
||||||
return try {
|
return try {
|
||||||
this.substring(start, end)
|
this.substring(start, end)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
"0.00"
|
"0.00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,7 +453,8 @@ fun NumberKeyboard(
|
|||||||
"backspace" -> KeyboardButton(
|
"backspace" -> KeyboardButton(
|
||||||
icon = painterResource(drawable.materialsymbols_ic_arrow_left_alt_outlined),
|
icon = painterResource(drawable.materialsymbols_ic_arrow_left_alt_outlined),
|
||||||
onClick = onBackspaceClick,
|
onClick = onBackspaceClick,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -488,19 +482,20 @@ fun NumberKeyboard(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KeyboardButton(
|
fun KeyboardButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
icon: Painter? = null,
|
icon: Painter? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
containerColor: Color = MaterialTheme.colorScheme.primary,
|
containerColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
contentColor: Color = MaterialTheme.colorScheme.onPrimary
|
contentColor: Color = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(4.dp)
|
.padding(2.dp)
|
||||||
.aspectRatio(2.5f),
|
.aspectRatio(2.5f),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -545,7 +540,12 @@ fun PreviewAddExpenseDisabled() {
|
|||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
expenseDtoToEdit = null,
|
expenseDtoToEdit = null,
|
||||||
state = sheetState,
|
state = sheetState,
|
||||||
currentTrip = Trip(1, "Trip", "2020-01-01", Currencies.entries.random().name),
|
currentTrip = Trip(
|
||||||
|
1,
|
||||||
|
"Trip",
|
||||||
|
LocalDate.parse("2020-01-01"),
|
||||||
|
Currencies.entries.random().name
|
||||||
|
),
|
||||||
categories = categoriesToPreview
|
categories = categoriesToPreview
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -572,13 +572,20 @@ fun PreviewAddExpenseEnabled() {
|
|||||||
amount = 10.31,
|
amount = 10.31,
|
||||||
currency = "PLN",
|
currency = "PLN",
|
||||||
note = "some note",
|
note = "some note",
|
||||||
datetime = "2025-11-30T10:16:26.939",
|
datetime = LocalDateTime.now(),
|
||||||
categoryId = 1,
|
categoryId = 1,
|
||||||
tripId = 1
|
tripId = 1
|
||||||
), category = categoriesToPreview.get(0), Trip(1, "Włochy", "2025-01-02", "PLN")
|
),
|
||||||
|
category = categoriesToPreview[0],
|
||||||
|
Trip(1, "Włochy", LocalDate.parse("2025-01-02"), "PLN")
|
||||||
),
|
),
|
||||||
state = sheetState,
|
state = sheetState,
|
||||||
currentTrip = Trip(1, "Trip", "2020-01-01", Currencies.entries.random().name),
|
currentTrip = Trip(
|
||||||
|
1,
|
||||||
|
"Trip",
|
||||||
|
LocalDate.parse("2020-01-01"),
|
||||||
|
Currencies.entries.random().name
|
||||||
|
),
|
||||||
categories = categoriesToPreview
|
categories = categoriesToPreview
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -611,4 +618,4 @@ val categoriesToPreview = listOf(
|
|||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
color = colors.random()
|
color = colors.random()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
@@ -40,7 +41,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -53,43 +53,39 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import cc.n0th1ng.tripmoney.R.string
|
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.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
import cc.n0th1ng.tripmoney.screens.addexpense.AddExpenseBottomSheet
|
||||||
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
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseDtoWithConvertedAmount
|
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel.ExpenseListItemUi
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||||
|
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun ListExpenseScreen() {
|
fun ListExpenseScreen() {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val currentTrip by settingsViewModel.currentTrip.collectAsState()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val expensesWithConvertedFlow = expenseAndCategoryViewModel
|
val expensesFlow = expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId)
|
||||||
.getExpensesWithConvertedAmountsPaged(currentTrip)
|
|
||||||
|
|
||||||
ListExpenseScreen(
|
ListExpenseScreen(
|
||||||
expensesWithConvertedFlow = expensesWithConvertedFlow,
|
expensesFlow = expensesFlow,
|
||||||
onSaveExpense = { expenseAndCategoryViewModel.save(it) },
|
onSaveExpense = { expenseAndCategoryViewModel.save(it, currentTrip!!) },
|
||||||
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) })
|
onDeleteExpense = { expenseAndCategoryViewModel.delete(it) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -97,14 +93,15 @@ fun ListExpenseScreen() {
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun ListExpenseScreen(
|
fun ListExpenseScreen(
|
||||||
expensesWithConvertedFlow: Flow<PagingData<ExpenseDtoWithConvertedAmount>>,
|
expensesFlow: Flow<PagingData<ExpenseListItemUi>>,
|
||||||
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit
|
onSaveExpense: (Expense) -> Unit, onDeleteExpense: (Expense) -> Unit
|
||||||
) {
|
) {
|
||||||
val expensesWithConverted = expensesWithConvertedFlow.collectAsLazyPagingItems()
|
|
||||||
|
val items = expensesFlow.collectAsLazyPagingItems()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var showBottomSheet by remember { mutableStateOf(false) }
|
var showBottomSheet by remember { mutableStateOf(false) }
|
||||||
var expenseDtoToEdit: ExpenseDto? = null
|
var expenseDtoToEdit by remember { mutableStateOf<ExpenseDto?>(null) }
|
||||||
val sumMap = remember { mutableStateMapOf<LocalDate, Double>() }
|
var itemToDelete by remember { mutableStateOf<Expense?>(null) }
|
||||||
|
|
||||||
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
Scaffold(floatingActionButtonPosition = FabPosition.EndOverlay, floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
@@ -114,57 +111,71 @@ fun ListExpenseScreen(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
LaunchedEffect(expensesWithConverted.itemSnapshotList.items) {
|
if (items.loadState.refresh == LoadState.Loading) {
|
||||||
val items = expensesWithConverted.itemSnapshotList.items
|
// Show loading indicator
|
||||||
val newSums = items
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
.groupBy { LocalDateTime.parse(it.expenseDto.expense.datetime).toLocalDate() }
|
CircularProgressIndicator()
|
||||||
.mapValues { (_, expensesForDay) ->
|
}
|
||||||
expensesForDay.sumOf { it.convertedAmount }
|
|
||||||
}
|
|
||||||
sumMap.clear()
|
|
||||||
sumMap.putAll(newSums)
|
|
||||||
}
|
}
|
||||||
LazyColumn(
|
else {
|
||||||
modifier = Modifier.fillMaxSize(),
|
LazyColumn(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = listState
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
state = listState
|
||||||
items(
|
) {
|
||||||
count = expensesWithConverted.itemCount,
|
items(
|
||||||
key = { index -> expensesWithConverted[index]?.expenseDto?.expense?.id ?: index }
|
count = items.itemCount,
|
||||||
) { index ->
|
key = items.itemKey { item ->
|
||||||
val expenseDtoWithConverted = expensesWithConverted[index]
|
when (item) {
|
||||||
val expenseDto = expenseDtoWithConverted?.expenseDto
|
is ExpenseListItemUi.Item -> item.expenseDto.expense.id
|
||||||
if (expenseDtoWithConverted != null && expenseDto != null) {
|
is ExpenseListItemUi.Header -> "header_${item.date}"
|
||||||
val previousExpense =
|
}
|
||||||
expensesWithConverted.itemSnapshotList.items.getOrNull(index - 1)?.expenseDto
|
}
|
||||||
val showDayDivider =
|
) { index ->
|
||||||
index == 0 || LocalDateTime.parse(previousExpense?.expense?.datetime)
|
|
||||||
.toLocalDate() != LocalDateTime.parse(expenseDto.expense.datetime)
|
when (val item = items[index]) {
|
||||||
.toLocalDate()
|
|
||||||
Spacer(Modifier
|
is ExpenseListItemUi.Header -> {
|
||||||
.height(5.dp)
|
CustomDivider(
|
||||||
.background(MaterialTheme.colorScheme.onBackground))
|
date = item.date,
|
||||||
if (showDayDivider) {
|
sum = item.sum,
|
||||||
CustomDivider(
|
currency = item.currency
|
||||||
expenseDto,
|
)
|
||||||
sumMap.getOrDefault(
|
}
|
||||||
LocalDateTime.parse(expenseDto.expense.datetime).toLocalDate(), 0.00
|
|
||||||
)
|
is ExpenseListItemUi.Item -> {
|
||||||
)
|
SwipeToDeleteExpenseCard(
|
||||||
|
expenseDto = item.expenseDto,
|
||||||
|
onDelete = { expense -> itemToDelete = expense },
|
||||||
|
onClick = { expenseDto ->
|
||||||
|
expenseDtoToEdit = expenseDto
|
||||||
|
showBottomSheet = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(5.dp))
|
|
||||||
SwipeToDeleteExpenseCard(
|
|
||||||
expenseDtoWithConverted = expenseDtoWithConverted,
|
|
||||||
onDelete = { expense -> onDeleteExpense(expense) },
|
|
||||||
onClick = { expenseDto ->
|
|
||||||
expenseDtoToEdit = expenseDto
|
|
||||||
showBottomSheet = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemToDelete != null) {
|
||||||
|
DeleteConfirmationDialog(
|
||||||
|
onConfirm = {
|
||||||
|
onDeleteExpense(itemToDelete!!)
|
||||||
|
itemToDelete = null
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
itemToDelete = null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
AddExpenseBottomSheet(
|
AddExpenseBottomSheet(
|
||||||
onSave = { expense ->
|
onSave = { expense ->
|
||||||
@@ -186,7 +197,7 @@ fun ListExpenseScreen(
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
|
fun CustomDivider(date: LocalDate, sum: Double, currency: String) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Absolute.Center,
|
horizontalArrangement = Arrangement.Absolute.Center,
|
||||||
@@ -194,7 +205,7 @@ fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
|
|||||||
) {
|
) {
|
||||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
Text(
|
Text(
|
||||||
LocalDateTime.parse(expenseDto.expense.datetime).format(
|
date.format(
|
||||||
DateTimeFormatter.ofPattern("dd EEEE")
|
DateTimeFormatter.ofPattern("dd EEEE")
|
||||||
).toString(),
|
).toString(),
|
||||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
||||||
@@ -209,7 +220,7 @@ fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
|
|||||||
) {
|
) {
|
||||||
HorizontalDivider(modifier = Modifier.weight(2f))
|
HorizontalDivider(modifier = Modifier.weight(2f))
|
||||||
Text(
|
Text(
|
||||||
"%.2f %s".format(sum, expenseDto.trip.currency),
|
"%.2f %s".format(sum, currency),
|
||||||
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
modifier = Modifier.background(Color.White.copy(alpha = 0f)),
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
@@ -222,55 +233,44 @@ fun CustomDivider(expenseDto: ExpenseDto, sum: Double) {
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun SwipeToDeleteExpenseCard(
|
fun SwipeToDeleteExpenseCard(
|
||||||
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
|
expenseDto: ExpenseDto,
|
||||||
onDelete: (Expense) -> Unit,
|
onDelete: (Expense) -> Unit,
|
||||||
onClick: (ExpenseDto) -> Unit
|
onClick: (ExpenseDto) -> Unit
|
||||||
) {
|
) {
|
||||||
var dismissed by remember { mutableStateOf(false) }
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
confirmValueChange = { dismissValue ->
|
||||||
|
if (dismissValue == SwipeToDismissBoxValue.EndToStart) {
|
||||||
if (!dismissed) {
|
onDelete(expenseDto.expense)
|
||||||
val dismissState = rememberSwipeToDismissBoxState(
|
false
|
||||||
confirmValueChange = { dismissValue ->
|
} else {
|
||||||
if (dismissValue == SwipeToDismissBoxValue.EndToStart
|
false
|
||||||
) {
|
|
||||||
showDialog = true
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
state = dismissState,
|
||||||
|
enableDismissFromStartToEnd = false,
|
||||||
|
backgroundContent = {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(CardDefaults.elevatedShape)
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.onError)
|
||||||
|
.padding(horizontal = 20.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ExpenseCard(
|
||||||
|
expenseDto = expenseDto,
|
||||||
|
onClick = onClick
|
||||||
)
|
)
|
||||||
if (showDialog) {
|
|
||||||
DeleteConfirmationDialog(
|
|
||||||
onConfirm = {
|
|
||||||
showDialog = false
|
|
||||||
dismissed = true
|
|
||||||
onDelete(expenseDtoWithConverted.expenseDto.expense)
|
|
||||||
},
|
|
||||||
onCancel = { showDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SwipeToDismissBox(
|
|
||||||
modifier = Modifier,
|
|
||||||
state = dismissState,
|
|
||||||
enableDismissFromStartToEnd = false,
|
|
||||||
backgroundContent = {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.clip(CardDefaults.elevatedShape)
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.onError)
|
|
||||||
.padding(horizontal = 20.dp),
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Delete, contentDescription = stringResource(string.delete))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
ExpenseCard(expenseDtoWithConverted, onClick = onClick)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,10 +324,9 @@ fun DeleteConfirmationDialog(
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExpenseCard(
|
fun ExpenseCard(
|
||||||
expenseDtoWithConverted: ExpenseDtoWithConvertedAmount,
|
expenseDto: ExpenseDto,
|
||||||
onClick: (ExpenseDto) -> Unit
|
onClick: (ExpenseDto) -> Unit
|
||||||
) {
|
) {
|
||||||
val expenseDto = expenseDtoWithConverted.expenseDto
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = CardDefaults.elevatedCardColors()
|
colors = CardDefaults.elevatedCardColors()
|
||||||
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
.copy(containerColor = MaterialTheme.colorScheme.secondaryContainer),
|
||||||
@@ -379,7 +378,7 @@ fun ExpenseCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = LocalDateTime.parse(expenseDto.expense.datetime).format(
|
text = expenseDto.expense.datetime.format(
|
||||||
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
DateTimeFormatter.ofPattern("dd MMM HH:mm")
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
@@ -397,7 +396,7 @@ fun ExpenseCard(
|
|||||||
)
|
)
|
||||||
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
if (expenseDto.expense.currency.lowercase() != expenseDto.trip.currency.lowercase()) {
|
||||||
Text(
|
Text(
|
||||||
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDtoWithConverted.convertedAmount),
|
text = "≈ %.2f ${expenseDto.trip.currency}".format(expenseDto.expense.convertedAmount()),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
@@ -408,106 +407,107 @@ fun ExpenseCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
//@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@AllPreviews
|
//@AllPreviews
|
||||||
@Composable
|
//@Composable
|
||||||
fun PreviewListExpenseScreen() {
|
//fun PreviewListExpenseScreen() {
|
||||||
TripMoneyTheme() {
|
// TripMoneyTheme() {
|
||||||
val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
|
// val pagingData = PagingData.from(sampleExpenseDtoWithConvertedAmountList())
|
||||||
ListExpenseScreen(
|
// ListExpenseScreen(
|
||||||
expensesWithConvertedFlow = MutableStateFlow(pagingData),
|
// expensesDtoFlow = MutableStateFlow(pagingData),
|
||||||
onSaveExpense = {},
|
// onSaveExpense = {},
|
||||||
onDeleteExpense = {}
|
// onDeleteExpense = {},
|
||||||
)
|
// dailySums = emptyMap()
|
||||||
|
// )
|
||||||
}
|
//
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
@AllPreviews
|
//
|
||||||
@Composable
|
//@AllPreviews
|
||||||
fun PreviewDeleteConfirmationDialog() {
|
//@Composable
|
||||||
TripMoneyTheme() {
|
//fun PreviewDeleteConfirmationDialog() {
|
||||||
DeleteConfirmationDialog(
|
// TripMoneyTheme() {
|
||||||
onConfirm = {},
|
// DeleteConfirmationDialog(
|
||||||
onCancel = {})
|
// onConfirm = {},
|
||||||
}
|
// onCancel = {})
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
//
|
||||||
private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseDtoWithConvertedAmount> {
|
//@RequiresApi(Build.VERSION_CODES.O)
|
||||||
val sampleCategories = listOf(
|
//private fun sampleExpenseDtoWithConvertedAmountList(): List<ExpenseDto> {
|
||||||
Category(
|
// val sampleCategories = listOf(
|
||||||
name = "Hotel",
|
// Category(
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
// name = "Hotel",
|
||||||
color = colors.random()
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.HOTEL,
|
||||||
),
|
// color = colors.random()
|
||||||
Category(
|
// ),
|
||||||
name = "Jedzenie",
|
// Category(
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
// name = "Jedzenie",
|
||||||
color = colors.random()
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.RESTAURANT,
|
||||||
),
|
// color = colors.random()
|
||||||
Category(
|
// ),
|
||||||
name = "Transport",
|
// Category(
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
// name = "Transport",
|
||||||
color = colors.random()
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.FLIGHT,
|
||||||
),
|
// color = colors.random()
|
||||||
Category(
|
// ),
|
||||||
name = "Rozrywka",
|
// Category(
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
// name = "Rozrywka",
|
||||||
color = colors.random()
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.ATTRACTION,
|
||||||
),
|
// color = colors.random()
|
||||||
Category(
|
// ),
|
||||||
name = "Zakupy",
|
// Category(
|
||||||
icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
// name = "Zakupy",
|
||||||
color = colors.random()
|
// icon = cc.n0th1ng.tripmoney.utils.Icons.GROCERIES,
|
||||||
),
|
// color = colors.random()
|
||||||
)
|
// ),
|
||||||
|
// )
|
||||||
val trip = Trip(
|
//
|
||||||
id = 1,
|
// val trip = Trip(
|
||||||
name = "Vacation",
|
// id = 1,
|
||||||
currency = "USD",
|
// name = "Vacation",
|
||||||
startDate = "2026-01-01"
|
// currency = "USD",
|
||||||
)
|
// startDate = LocalDate.parse("2026-01-01")
|
||||||
|
// )
|
||||||
val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
|
//
|
||||||
val endLong = LocalDateTime.now().toEpochMilli()
|
// val startLong = LocalDateTime.now().minusDays(10).toEpochMilli()
|
||||||
|
// val endLong = LocalDateTime.now().toEpochMilli()
|
||||||
val result: MutableList<ExpenseDtoWithConvertedAmount> = mutableListOf()
|
//
|
||||||
for (i in 0..15) {
|
// val result: MutableList<ExpenseDto> = mutableListOf()
|
||||||
val category = sampleCategories.random()
|
// for (i in 0..15) {
|
||||||
val datetime = if (i > 4) {
|
// val category = sampleCategories.random()
|
||||||
LocalDateTime.ofEpochSecond(
|
// val datetime = if (i > 4) {
|
||||||
Random.nextLong(startLong, endLong),
|
// LocalDateTime.ofEpochSecond(
|
||||||
0,
|
// Random.nextLong(startLong, endLong),
|
||||||
ZoneOffset.UTC
|
// 0,
|
||||||
).toString()
|
// ZoneOffset.UTC
|
||||||
} else LocalDateTime.now().toString()
|
// )
|
||||||
|
// } else LocalDateTime.now()
|
||||||
val expense = Expense(
|
//
|
||||||
id = i,
|
// val expense = Expense(
|
||||||
categoryId = category.id,
|
// id = i,
|
||||||
tripId = 1,
|
// categoryId = category.id,
|
||||||
amount = Random.nextDouble(0.1, 300.0),
|
// tripId = 1,
|
||||||
currency = Currencies.entries.random().name,
|
// amount = Random.nextDouble(0.1, 300.0),
|
||||||
note = if (i % 3 == 0) "Some note" else "",
|
// currency = Currencies.entries.random().name,
|
||||||
datetime = datetime
|
// note = if (i % 3 == 0) "Some note" else "",
|
||||||
)
|
// datetime = datetime,
|
||||||
val expenseDto = ExpenseDto(
|
// rate = if (Random.nextBoolean()) Random.nextDouble(
|
||||||
expense = expense,
|
// 0.1,
|
||||||
category = category,
|
// 5.0
|
||||||
trip = trip
|
// ) else 1.0
|
||||||
)
|
// )
|
||||||
result.add(
|
//
|
||||||
ExpenseDtoWithConvertedAmount(
|
//
|
||||||
expenseDto,
|
// val expenseDto = ExpenseDto(
|
||||||
convertedAmount = if (Random.nextBoolean()) Random.nextDouble(
|
// expense = expense,
|
||||||
0.1,
|
// category = category,
|
||||||
300.0
|
// trip = trip
|
||||||
) else expense.amount
|
// )
|
||||||
)
|
// result.add(
|
||||||
)
|
// expenseDto
|
||||||
}
|
// )
|
||||||
return result
|
// }
|
||||||
}
|
// return result
|
||||||
|
//}
|
||||||
@@ -39,6 +39,7 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import cc.n0th1ng.tripmoney.R.*
|
import cc.n0th1ng.tripmoney.R.*
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
import cc.n0th1ng.tripmoney.data.repository.AppTheme
|
||||||
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
|
||||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||||
@@ -66,7 +67,7 @@ fun SettingsScreen() {
|
|||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTrip = tripViewModel.getTrip(currentTripId)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val tripName = currentTrip?.name ?: ""
|
val tripName = currentTrip?.name ?: ""
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ fun StatisticsScreen() {
|
|||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip = tripViewModel.getTrip(currentTripId)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
|
val summaryPerCategoryList by expenseAndCategoryViewModel.getSummaryPerCategory(currentTripId)
|
||||||
.collectAsState(emptyList())
|
.collectAsState(emptyList())
|
||||||
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
|
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun AddTripBottomSheet(
|
|||||||
var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
|
var name by remember { mutableStateOf(tripToEdit?.name ?: "") }
|
||||||
var startDate by remember {
|
var startDate by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
LocalDate.parse(tripToEdit?.startDate ?: LocalDate.now().toString())
|
tripToEdit?.startDate ?: LocalDate.now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ fun AddTripBottomSheet(
|
|||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
onClick = {
|
onClick = {
|
||||||
val trip =
|
val trip =
|
||||||
Trip(name = name, startDate = startDate.toString(), currency = currency)
|
Trip(name = name, startDate = startDate, currency = currency)
|
||||||
|
|
||||||
onSave(if (tripToEdit == null) trip else trip.copy(id = tripToEdit.id))
|
onSave(if (tripToEdit == null) trip else trip.copy(id = tripToEdit.id))
|
||||||
}) {
|
}) {
|
||||||
@@ -231,7 +231,7 @@ fun PreviewAddTripBottomSheetEditTrip() {
|
|||||||
AddTripBottomSheet(
|
AddTripBottomSheet(
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
Trip(1, "Włochy", "2025-01-02", "PLN"),
|
Trip(1, "Włochy", LocalDate.parse("2025-01-02"), "PLN"),
|
||||||
sheetState,
|
sheetState,
|
||||||
defaultCurrency = Currencies.entries.random()
|
defaultCurrency = Currencies.entries.random()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ fun TripCard(
|
|||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name)
|
Text(fontSize = 25.sp, fontWeight = FontWeight.SemiBold, text = trip.name)
|
||||||
Text(trip.startDate)
|
Text(trip.startDate.toString())
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
trip.currency.uppercase(),
|
trip.currency.uppercase(),
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.insertSeparators
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
||||||
import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository
|
import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository
|
||||||
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
||||||
@@ -18,15 +20,16 @@ import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
|||||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.apache.commons.csv.CSVFormat
|
import org.apache.commons.csv.CSVFormat
|
||||||
import org.apache.commons.csv.CSVPrinter
|
import org.apache.commons.csv.CSVPrinter
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDate
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.mapValues
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -37,15 +40,63 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
private val tripRepo: TripRepository
|
private val tripRepo: TripRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
fun getExpenses(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
fun getExpensesDtoPaged(tripId: Int): Flow<PagingData<ExpenseDto>> =
|
||||||
expenseRepo.getExpensesPaged(tripId).cachedIn(viewModelScope)
|
expenseRepo.getExpensesDtoPaged(tripId).cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun save(expense: Expense) {
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun getExpensesWithHeadersPaged(
|
||||||
|
tripId: Int
|
||||||
|
): Flow<PagingData<ExpenseListItemUi>> {
|
||||||
|
val pagingFlow = getExpensesDtoPaged(tripId)
|
||||||
|
val sumsFlow = getDailySums(tripId)
|
||||||
|
val tripFlow = tripRepo.getTrip(tripId)
|
||||||
|
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
||||||
|
val currency = trip?.currency ?: ""
|
||||||
|
pagingData
|
||||||
|
.map<ExpenseDto, ExpenseListItemUi> {
|
||||||
|
ExpenseListItemUi.Item(it)
|
||||||
|
}
|
||||||
|
.insertSeparators { before, after ->
|
||||||
|
if (after == null) return@insertSeparators null
|
||||||
|
val afterItem = after as ExpenseListItemUi.Item
|
||||||
|
val afterDate = afterItem.expenseDto.expense.datetime.toLocalDate()
|
||||||
|
val beforeDate = (before as? ExpenseListItemUi.Item)
|
||||||
|
?.expenseDto
|
||||||
|
?.expense
|
||||||
|
?.datetime
|
||||||
|
?.toLocalDate()
|
||||||
|
|
||||||
|
if (before == null || beforeDate != afterDate) {
|
||||||
|
ExpenseListItemUi.Header(
|
||||||
|
date = afterDate,
|
||||||
|
sum = sums[afterDate] ?: 0.0,
|
||||||
|
currency = currency
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExpensesDto(tripId: Int): Flow<List<ExpenseDto>> =
|
||||||
|
expenseRepo.getExpensesDto(tripId)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun save(expense: Expense, trip: Trip) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
expenseRepo.save(expense)
|
val rate = exchangeRateRepository.getRate(
|
||||||
|
Currencies.valueOf(expense.currency),
|
||||||
|
Currencies.valueOf(trip.currency),
|
||||||
|
expense.datetime.toLocalDate()
|
||||||
|
)
|
||||||
|
expenseRepo.save(expense.copy(rate = rate))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun delete(expense: Expense) {
|
fun delete(expense: Expense) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
expenseRepo.delete(expense)
|
expenseRepo.delete(expense)
|
||||||
@@ -67,7 +118,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
writer,
|
writer,
|
||||||
CSVFormat.DEFAULT.withHeader("date", "category", "currency", "amount")
|
CSVFormat.DEFAULT.withHeader("date", "category", "currency", "amount")
|
||||||
).use { printer ->
|
).use { printer ->
|
||||||
expenseRepo.getExpenses(tripId).first().forEach { expenseDto ->
|
expenseRepo.getExpensesDto(tripId).first().forEach { expenseDto ->
|
||||||
printer.printRecord(
|
printer.printRecord(
|
||||||
expenseDto.expense.datetime,
|
expenseDto.expense.datetime,
|
||||||
expenseDto.category.name,
|
expenseDto.category.name,
|
||||||
@@ -80,68 +131,45 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun getDailySums(tripId: Int): Flow<Map<LocalDate, Double>> {
|
||||||
|
return getExpensesDto(tripId)
|
||||||
|
.map { expenses ->
|
||||||
|
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
||||||
|
.mapValues { (_, list) ->
|
||||||
|
list.sumOf { it.expense.amount * it.expense.rate }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getSummaryAmount(tripId: Int): Flow<Double> {
|
fun getSummaryAmount(tripId: Int): Flow<Double> {
|
||||||
return getExpensesWithConvertedAmounts(tripId).map { list ->
|
return getExpensesDto(tripId).map { list ->
|
||||||
list.sumOf { it.convertedAmount }
|
list.sumOf { it.expense.amount * it.expense.rate }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
|
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategory>> {
|
||||||
val tripCurrency = tripRepo.getTrip(tripId)?.currency ?: Currencies.default().name
|
val tripFlow = tripRepo.getTrip(tripId)
|
||||||
return getExpensesWithConvertedAmounts(tripId)
|
val expensesFlow = getExpensesDto(tripId)
|
||||||
.map { list ->
|
|
||||||
val sumOfAll = list.sumOf { it.convertedAmount }
|
|
||||||
list.groupBy { it.expenseDto.category }
|
|
||||||
.map { (category, expenses) ->
|
|
||||||
val total = expenses.sumOf { it.convertedAmount }
|
|
||||||
SummaryPerCategory(
|
|
||||||
category = category,
|
|
||||||
amount = total,
|
|
||||||
percent = (total / sumOfAll).toFloat(),
|
|
||||||
currency = Currencies.valueOf(tripCurrency)
|
|
||||||
)
|
|
||||||
}.sortedBy { it.percent }.reversed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return tripFlow.combine(expensesFlow) { trip, expenses ->
|
||||||
|
val tripCurrency = trip?.currency ?: Currencies.default().name
|
||||||
|
val sumOfAll = expenses.sumOf { it.expense.convertedAmount() }
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
expenses.groupBy { it.category }
|
||||||
fun getExpensesWithConvertedAmounts(tripId: Int): Flow<List<ExpenseDtoWithConvertedAmount>> {
|
.map { (category, expensesForCategory) ->
|
||||||
return expenseRepo.getExpenses(tripId)
|
val total = expensesForCategory.sumOf { it.expense.convertedAmount() }
|
||||||
.map { list ->
|
SummaryPerCategory(
|
||||||
list.map { expenseDto ->
|
category = category,
|
||||||
val convertedAmount =
|
amount = total,
|
||||||
if (expenseDto.expense.currency != expenseDto.trip.currency) {
|
percent = (total / sumOfAll).toFloat(),
|
||||||
runBlocking {
|
currency = Currencies.valueOf(tripCurrency)
|
||||||
expenseDto.convertedAmount()
|
)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expenseDto.expense.amount
|
|
||||||
}
|
|
||||||
ExpenseDtoWithConvertedAmount(expenseDto, convertedAmount)
|
|
||||||
}
|
}
|
||||||
}
|
.sortedByDescending { it.percent }
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
fun getExpensesWithConvertedAmountsPaged(tripId: Int): Flow<PagingData<ExpenseDtoWithConvertedAmount>> {
|
|
||||||
return expenseRepo.getExpensesPaged(tripId)
|
|
||||||
.map { pagingData ->
|
|
||||||
pagingData.map { expenseDto ->
|
|
||||||
val convertedAmount =
|
|
||||||
if (expenseDto.expense.currency != expenseDto.trip.currency) {
|
|
||||||
runBlocking {
|
|
||||||
expenseDto.convertedAmount()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expenseDto.expense.amount
|
|
||||||
}
|
|
||||||
ExpenseDtoWithConvertedAmount(expenseDto, convertedAmount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@@ -152,16 +180,9 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
suspend fun ExpenseDto.convertedAmount(): Double {
|
sealed class ExpenseListItemUi {
|
||||||
return exchangeRateRepository.getRate(
|
data class Item(val expenseDto: ExpenseDto) : ExpenseListItemUi()
|
||||||
Currencies.valueOf(this.expense.currency),
|
data class Header(val date: LocalDate, val sum: Double, val currency: String) : ExpenseListItemUi()
|
||||||
Currencies.valueOf(this.trip.currency),
|
|
||||||
LocalDateTime.parse(this.expense.datetime).toLocalDate()
|
|
||||||
) * this.expense.amount
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ExpenseDtoWithConvertedAmount(
|
|
||||||
val expenseDto: ExpenseDto,
|
|
||||||
val convertedAmount: Double
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ class TripViewModel @Inject constructor(private val repository: TripRepository)
|
|||||||
|
|
||||||
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
|
fun getTrips(): Flow<PagingData<Trip>> = repository.getTrips().cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun getTrip(tripId: Int): Trip? = repository.getTrip(tripId)
|
fun getTrip(tripId: Int): Flow<Trip?> = repository.getTrip(tripId)
|
||||||
|
|
||||||
fun delete(trip: Trip) {
|
fun delete(trip: Trip) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
Reference in New Issue
Block a user