This commit is contained in:
Rafal Wisniewski
2026-03-26 22:38:51 +01:00
parent af7c926060
commit 9b19b100e9
18 changed files with 488 additions and 388 deletions

View 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)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.annotation.RequiresApi
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
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.utils.Icons
import cc.n0th1ng.tripmoney.utils.colors
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -25,11 +27,13 @@ import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.LocalDateTime
import javax.inject.Inject
import javax.inject.Singleton
@Database(entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1)
@TypeConverters(Converters::class)
abstract class TripDatabase : RoomDatabase() {
abstract fun tripDao(): TripDao
abstract fun expenseDao(): ExpenseDao
@@ -46,7 +50,8 @@ object DatabaseModule {
@Provides
@Singleton
fun provideTripDatabase(
@ApplicationContext context: Context
@ApplicationContext context: Context,
// expenseAndCategoryViewModel: ExpenseAndCategoryViewModel
): TripDatabase {
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
@@ -94,9 +99,9 @@ private class DatabasePrepopulator(
) {
@RequiresApi(Build.VERSION_CODES.O)
suspend fun prepopulate() {
tripDao.insert(Trip(name = "Włochy", startDate = "2026-03-01", currency = "PLN"))
tripDao.insert(Trip(name = "Szwajcaria", startDate = "2025-03-01", currency = "EUR"))
tripDao.insert(Trip(name = "Portugalia", startDate = "2025-03-01", currency = "USD"))
tripDao.insert(Trip(name = "Włochy", startDate = LocalDate.parse("2026-03-01"), currency = "PLN"))
tripDao.insert(Trip(name = "Szwajcaria", startDate =LocalDate.parse("2025-03-01"), currency = "EUR"))
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 = "Transport", icon = Icons.TRANSPORT, color = colors.random()))
categoryDao.insert(Category(name = "Flight", icon = Icons.FLIGHT, color = colors.random()))
@@ -113,7 +118,7 @@ private class DatabasePrepopulator(
amount = 120.50,
currency = "PLN",
note = "Hotel overnight",
datetime = now.minusDays(10).toString(),
datetime = now.minusDays(10),
categoryId = 1,
tripId = 1
)
@@ -123,7 +128,7 @@ private class DatabasePrepopulator(
amount = 45.75,
currency = "PLN",
note = "Dinner",
datetime = now.minusDays(9).toString(),
datetime = now.minusDays(9),
categoryId = 2,
tripId = 1
)
@@ -133,7 +138,7 @@ private class DatabasePrepopulator(
amount = 15.20,
currency = "PLN",
note = "Bus ticket",
datetime = now.minusDays(8).toString(),
datetime = now.minusDays(8),
categoryId = 3,
tripId = 1
)
@@ -143,7 +148,7 @@ private class DatabasePrepopulator(
amount = 89.99,
currency = "PLN",
note = "Concert tickets",
datetime = now.minusDays(7).toString(),
datetime = now.minusDays(7),
categoryId = 4,
tripId = 1
)
@@ -153,7 +158,7 @@ private class DatabasePrepopulator(
amount = 32.50,
currency = "PLN",
note = "Souvenirs",
datetime = now.minusDays(6).toString(),
datetime = now.minusDays(6),
categoryId = 5,
tripId = 1
)
@@ -163,7 +168,7 @@ private class DatabasePrepopulator(
amount = 180.00,
currency = "PLN",
note = "Hotel 3 nights",
datetime = now.minusDays(5).toString(),
datetime = now.minusDays(5),
categoryId = 1,
tripId = 1
)
@@ -173,7 +178,7 @@ private class DatabasePrepopulator(
amount = 67.30,
currency = "PLN",
note = "Lunch",
datetime = now.minusDays(4).toString(),
datetime = now.minusDays(4),
categoryId = 2,
tripId = 1
)
@@ -183,7 +188,7 @@ private class DatabasePrepopulator(
amount = 22.00,
currency = "PLN",
note = "Train ticket",
datetime = now.minusDays(3).toString(),
datetime = now.minusDays(3),
categoryId = 3,
tripId = 1
)
@@ -193,7 +198,7 @@ private class DatabasePrepopulator(
amount = 55.00,
currency = "PLN",
note = "Museum entry",
datetime = now.minusDays(2).toString(),
datetime = now.minusDays(2),
categoryId = 4,
tripId = 1
)
@@ -203,7 +208,7 @@ private class DatabasePrepopulator(
amount = 12.99,
currency = "PLN",
note = "Snacks",
datetime = now.minusDays(1).toString(),
datetime = now.minusDays(1),
categoryId = 2,
tripId = 1
)
@@ -213,7 +218,7 @@ private class DatabasePrepopulator(
amount = 210.00,
currency = "PLN",
note = "Hotel 5 nights",
datetime = now.toString(),
datetime = now,
categoryId = 1,
tripId = 1
)
@@ -223,7 +228,7 @@ private class DatabasePrepopulator(
amount = 95.50,
currency = "EUR",
note = "Dinner for two",
datetime = now.minusHours(12).toString(),
datetime = now.minusHours(12),
categoryId = 2,
tripId = 1
)
@@ -233,7 +238,7 @@ private class DatabasePrepopulator(
amount = 30.00,
currency = "EUR",
note = "Taxi",
datetime = now.minusHours(6).toString(),
datetime = now.minusHours(6),
categoryId = 3,
tripId = 1
)
@@ -243,7 +248,7 @@ private class DatabasePrepopulator(
amount = 40.00,
currency = "USD",
note = "Gifts",
datetime = now.minusHours(3).toString(),
datetime = now.minusHours(3),
categoryId = 5,
tripId = 1
)
@@ -253,7 +258,7 @@ private class DatabasePrepopulator(
amount = 75.00,
currency = "PLN",
note = "Sightseeing tour",
datetime = now.minusHours(1).toString(),
datetime = now.minusHours(1),
categoryId = 4,
tripId = 1
)

View File

@@ -19,7 +19,7 @@ interface ExpenseDao {
@Query(
"""
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>
@@ -28,33 +28,11 @@ interface ExpenseDao {
@Query(
"""
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>>
@Delete
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>>
}

View File

@@ -7,6 +7,7 @@ import androidx.room.Insert
import androidx.room.Query
import androidx.room.Upsert
import cc.n0th1ng.tripmoney.data.entity.Trip
import kotlinx.coroutines.flow.Flow
@Dao
interface TripDao {
@@ -27,5 +28,5 @@ interface TripDao {
@Query(
"SELECT * FROM trip where trip.id = :tripId"
)
fun trip(tripId: Int): Trip?
fun trip(tripId: Int): Flow<Trip?>
}

View File

@@ -5,6 +5,7 @@ import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.Relation
import java.time.LocalDateTime
@Entity(tableName = "expense")
data class Expense(
@@ -12,10 +13,15 @@ data class Expense(
@ColumnInfo("amount") val amount: Double,
@ColumnInfo("currency") val currency: String,
@ColumnInfo("note") val note: String,
@ColumnInfo("datetime") val datetime: String,
@ColumnInfo("datetime") val datetime: LocalDateTime,
@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(
@Embedded val expense: Expense,

View File

@@ -1,13 +1,22 @@
package cc.n0th1ng.tripmoney.data.entity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import cc.n0th1ng.tripmoney.utils.Currencies
import java.time.LocalDate
@Entity(tableName = "trip")
data class Trip(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo("name") val name: String,
@ColumnInfo("start_date") val startDate: String,
@ColumnInfo("start_date") val startDate: LocalDate,
@ColumnInfo("currency") val currency: String
)
){
companion object {
@RequiresApi(Build.VERSION_CODES.O)
val DUMMY = Trip(-1, "dummy", LocalDate.now(), Currencies.default().name)
}
}

View File

@@ -25,6 +25,7 @@ class ExchangeRateRepository @Inject constructor(
@RequiresApi(Build.VERSION_CODES.O)
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 cachedRate = exchangeRateDao.getById(id)
return if (cachedRate != null) {

View File

@@ -1,6 +1,9 @@
package cc.n0th1ng.tripmoney.data.repository
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
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.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.utils.Currencies
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
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
suspend fun save(expense: Expense) {
@@ -23,18 +32,30 @@ class ExpenseRepository @Inject constructor(private val expenseDao: ExpenseDao)
expenseDao.delete(expense)
}
fun getExpensesPaged(tripId: Int): Flow<PagingData<ExpenseDto>> {
fun getExpensesDtoPaged(tripId: Int): Flow<PagingData<ExpenseDto>> {
return Pager(
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId) }
).flow
}
fun getExpenses(tripId: Int): Flow<List<ExpenseDto>> {
fun getExpensesDto(tripId: Int): Flow<List<ExpenseDto>> {
return expenseDao.expenseDto(tripId)
}
fun getSummaryPerCategory(tripId: Int): Flow<List<SummaryPerCategoryRaw>> {
return expenseDao.summaryPerCategoryRaw(tripId)
@RequiresApi(Build.VERSION_CODES.O)
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)
)
}
}
}

View File

@@ -1,18 +1,26 @@
package cc.n0th1ng.tripmoney.data.repository
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import cc.n0th1ng.tripmoney.data.dao.TripDao
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import kotlinx.coroutines.flow.Flow
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
suspend fun save(trip: Trip) {
expenseRepository.recalculateTripExpenses(trip.id)
tripDao.insert(trip)
}
@@ -23,7 +31,7 @@ class TripRepository @Inject constructor(private val tripDao: TripDao) {
).flow
}
fun getTrip(tripId: Int): Trip? {
fun getTrip(tripId: Int): Flow<Trip?> {
return tripDao.trip(tripId)
}