feat: add import from CSV and snackbar
This commit is contained in:
@@ -10,8 +10,11 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -41,6 +44,7 @@ import cc.n0th1ng.tripmoney.screens.trippicker.TripPickerScreen
|
||||
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SnackbarViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -80,9 +84,17 @@ fun NavigationDrawer() {
|
||||
val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState()
|
||||
var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) }
|
||||
val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val snackbarViewModel: SnackbarViewModel = hiltViewModel()
|
||||
LaunchedEffect(Unit) {
|
||||
snackbarViewModel.snackbarManager.messages.collect { message ->
|
||||
snackbarHostState.showSnackbar(message, withDismissAction = true)
|
||||
}
|
||||
}
|
||||
ReportDrawnWhen { !categories.isEmpty() }
|
||||
CustomNavigationDrawer(navController, drawerState) {
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
topBar = {
|
||||
if (current == Screens.SETTINGS) TopBarSettings(
|
||||
navController
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package cc.n0th1ng.tripmoney.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
@@ -49,7 +47,6 @@ abstract class TripDatabase : RoomDatabase() {
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTripDatabase(
|
||||
@@ -76,7 +73,6 @@ object DatabaseModule {
|
||||
).prepopulate()
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -111,7 +107,6 @@ private class DatabasePrepopulator(
|
||||
private val categoryDao: CategoryDao,
|
||||
private val expenseDao: ExpenseDao
|
||||
) {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun prepopulate() {
|
||||
|
||||
tripDao.insert(
|
||||
@@ -193,7 +188,6 @@ private class DatabasePrepopulator(
|
||||
),
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
val sampleExpenses = (0..150).map { i ->
|
||||
|
||||
val datetime = if (i > 4) {
|
||||
|
||||
@@ -11,9 +11,14 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface CategoryDao {
|
||||
@Upsert
|
||||
suspend fun insert(category: Category)
|
||||
|
||||
suspend fun insert(category: Category): Long
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM category WHERE name = :name LIMIT 1
|
||||
"""
|
||||
)
|
||||
fun getByName(name: String): Flow<Category?>
|
||||
@Delete
|
||||
suspend fun delete(category: Category)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package cc.n0th1ng.tripmoney.data.dao
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
@@ -12,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
@Dao
|
||||
interface TripDao {
|
||||
@Upsert
|
||||
suspend fun insert(trip: Trip)
|
||||
suspend fun insert(trip: Trip): Long
|
||||
|
||||
@Query(
|
||||
"""
|
||||
|
||||
@@ -9,8 +9,12 @@ import javax.inject.Inject
|
||||
class CategoryRepository @Inject constructor(private val categoryDao: CategoryDao) {
|
||||
|
||||
@WorkerThread
|
||||
suspend fun save(category: Category) {
|
||||
categoryDao.insert(category)
|
||||
suspend fun save(category: Category): Long {
|
||||
return categoryDao.insert(category)
|
||||
}
|
||||
|
||||
fun getByName(name: String): Flow<Category?> {
|
||||
return categoryDao.getByName(name)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package cc.n0th1ng.tripmoney.data.repository
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
|
||||
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.ExchangeRate
|
||||
import cc.n0th1ng.tripmoney.service.ExchangeService
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -23,7 +18,6 @@ class ExchangeRateRepository @Inject constructor(
|
||||
exchangeRateDao.insert(exchangeRate)
|
||||
}
|
||||
|
||||
@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())
|
||||
@@ -45,7 +39,6 @@ class ExchangeRateRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun clearOldRates(daysToKeep: Int = 180) {
|
||||
val cutoffDate = LocalDate.now().minusDays(daysToKeep.toLong()).toString()
|
||||
exchangeRateDao.deleteOldRates(cutoffDate)
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class TripRepository @Inject constructor(
|
||||
private val tripDao: TripDao,
|
||||
private val expenseRepository: ExpenseRepository
|
||||
private val tripDao: TripDao
|
||||
) {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
suspend fun save(trip: Trip) {
|
||||
tripDao.insert(trip)
|
||||
suspend fun save(trip: Trip): Long {
|
||||
return tripDao.insert(trip)
|
||||
}
|
||||
|
||||
fun getTrips(): Flow<PagingData<Trip>> {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package cc.n0th1ng.tripmoney.screens.addexpense
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.focusable
|
||||
@@ -77,7 +75,6 @@ import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> Unit,
|
||||
@@ -104,7 +101,6 @@ fun AddExpenseBottomSheet(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Composable
|
||||
fun AddExpenseBottomSheet(
|
||||
onSave: (Expense) -> Unit,
|
||||
@@ -558,7 +554,6 @@ val keyboard = listOf(
|
||||
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
@@ -589,7 +584,6 @@ fun PreviewAddExpenseDisabled() {
|
||||
}
|
||||
|
||||
@SuppressLint("CoroutineCreationDuringComposition")
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
|
||||
@@ -100,7 +100,6 @@ fun ListExpenseScreen(
|
||||
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter)
|
||||
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
|
||||
var idToScroll by remember { mutableIntStateOf(-1) }
|
||||
|
||||
ListExpenseScreen(
|
||||
currentTrip = currentTrip,
|
||||
expensesFlow = expensesFlow,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package cc.n0th1ng.tripmoney.screens.settings
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -48,6 +53,7 @@ import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.utils.shareCsv
|
||||
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.SnackbarViewModel
|
||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||
import com.composables.icons.materialsymbols.outlined.R
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -57,6 +63,7 @@ import java.io.File
|
||||
@Composable
|
||||
fun SettingsScreen(navController: NavHostController) {
|
||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||
val snackbarViewModel: SnackbarViewModel = hiltViewModel()
|
||||
val currentTheme by settingsViewModel.theme.collectAsState()
|
||||
val currentAddExpenseSwitch by settingsViewModel.addExpenseSwitch.collectAsState()
|
||||
val currentDefaultCurrency by settingsViewModel.defaultCurrency.collectAsState()
|
||||
@@ -67,7 +74,31 @@ fun SettingsScreen(navController: NavHostController) {
|
||||
val context = LocalContext.current
|
||||
val tripName = currentTrip?.name ?: ""
|
||||
val scope = rememberCoroutineScope()
|
||||
val result = remember { mutableStateOf<Uri?>(null) }
|
||||
val wentWrongMessage = stringResource(string.went_wrong)
|
||||
val successImportMessage = stringResource(string.import_success)
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
result.value = it
|
||||
if (it != null) {
|
||||
context.contentResolver.openInputStream(it)?.use { inputStream ->
|
||||
inputStream.bufferedReader().use { input ->
|
||||
val csvText = input.readText()
|
||||
val filename = context.contentResolver.filename(it)
|
||||
if (filename != null) {
|
||||
expenseAndCategoryViewModel.importCSV(
|
||||
csvText, filename,
|
||||
onError = { ex ->
|
||||
snackbarViewModel.showMessage(wentWrongMessage)
|
||||
},
|
||||
onSuccess = {
|
||||
snackbarViewModel.showMessage(successImportMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
SettingsScreen(
|
||||
currentDefaultCurrency = currentDefaultCurrency,
|
||||
currentTheme = currentTheme,
|
||||
@@ -90,11 +121,29 @@ fun SettingsScreen(navController: NavHostController) {
|
||||
}
|
||||
}
|
||||
},
|
||||
onImportCsv = {
|
||||
launcher.launch(arrayOf("*/*"))
|
||||
},
|
||||
onCategoriesClick = { navController.navigate(Screens.MANAGE_CATEGORIES) },
|
||||
currentAddExpenseSwitch = currentAddExpenseSwitch
|
||||
)
|
||||
}
|
||||
|
||||
private fun ContentResolver.filename(uri: Uri): String? {
|
||||
val projection = arrayOf<String?>(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||
val metaCursor = this.query(uri, projection, null, null, null);
|
||||
if (metaCursor != null) {
|
||||
try {
|
||||
if (metaCursor.moveToFirst()) {
|
||||
return metaCursor.getString(0)
|
||||
}
|
||||
} finally {
|
||||
metaCursor.close();
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -106,7 +155,8 @@ fun SettingsScreen(
|
||||
onExportToCsv: () -> Unit,
|
||||
onCategoriesClick: () -> Unit,
|
||||
onAddExpenseSwitch: (Boolean) -> Unit,
|
||||
currentAddExpenseSwitch: Boolean
|
||||
currentAddExpenseSwitch: Boolean,
|
||||
onImportCsv: () -> Unit
|
||||
) {
|
||||
|
||||
Scaffold { padding ->
|
||||
@@ -143,12 +193,20 @@ fun SettingsScreen(
|
||||
iconResource = R.drawable.materialsymbols_ic_palette_outlined
|
||||
)
|
||||
}
|
||||
SettingsCard(string.import_export) {
|
||||
SettingsListItem(
|
||||
onClick = onExportToCsv,
|
||||
stringResource(string.export_to_csv),
|
||||
supportingText = stringResource(string.export_csv_subttext).format(tripName),
|
||||
iconResource = R.drawable.materialsymbols_ic_csv_outlined
|
||||
)
|
||||
SettingsListItem(
|
||||
onClick = onImportCsv,
|
||||
stringResource(string.import_csv),
|
||||
supportingText = stringResource(string.import_csv_subtext),
|
||||
iconResource = R.drawable.materialsymbols_ic_csv_outlined
|
||||
)
|
||||
}
|
||||
SettingsListItem(
|
||||
onClick = onCategoriesClick,
|
||||
stringResource(string.categories),
|
||||
@@ -161,7 +219,9 @@ fun SettingsScreen(
|
||||
supportingText = stringResource(string.add_expense_settings),
|
||||
iconResource = R.drawable.materialsymbols_ic_payments_outlined,
|
||||
trailingContent = {
|
||||
Switch(checked = currentAddExpenseSwitch, onCheckedChange = {onAddExpenseSwitch(it)})
|
||||
Switch(
|
||||
checked = currentAddExpenseSwitch,
|
||||
onCheckedChange = { onAddExpenseSwitch(it) })
|
||||
}
|
||||
)
|
||||
|
||||
@@ -287,11 +347,13 @@ fun PreviewSettingsScreen() {
|
||||
tripName = "Włochy",
|
||||
onCategoriesClick = {},
|
||||
onAddExpenseSwitch = {},
|
||||
currentAddExpenseSwitch = false
|
||||
currentAddExpenseSwitch = false,
|
||||
onImportCsv = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@AllPreviews
|
||||
@Composable
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package cc.n0th1ng.tripmoney.service
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import cc.n0th1ng.tripmoney.data.TripDatabase
|
||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||
import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import cc.n0th1ng.tripmoney.utils.Icons
|
||||
import cc.n0th1ng.tripmoney.utils.colors
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.apache.commons.csv.CSVFormat
|
||||
import org.apache.commons.csv.CSVParser
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
class ImportService @Inject() constructor(
|
||||
private val tripRepo: TripRepository,
|
||||
private val expenseRepository: ExpenseRepository,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val exchangeRateRepository: ExchangeRateRepository,
|
||||
private val db: TripDatabase
|
||||
) {
|
||||
suspend fun importCSV(
|
||||
csv: String,
|
||||
filename: String,
|
||||
onError: (Exception) -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
try {
|
||||
db.withTransaction {
|
||||
val parser = CSVParser.parse(
|
||||
csv, CSVFormat.DEFAULT.builder().setHeader(
|
||||
"date", "category", "currency", "amount", "note"
|
||||
).setSkipHeaderRecord(true).get()
|
||||
)
|
||||
val records = parser.records.toList()
|
||||
val currency = records.map { it.get("currency") }.groupingBy { it }.eachCount()
|
||||
.maxBy { it.value }.key
|
||||
val startDate =
|
||||
records.map { it.get("date") }
|
||||
.minOfOrNull { LocalDateTime.parse(it.substringBefore(",")).toLocalDate() }
|
||||
val endDate =
|
||||
records.map { it.get("date") }
|
||||
.maxOfOrNull { LocalDateTime.parse(it.substringBefore(",")).toLocalDate() }
|
||||
if (!Currencies.names().contains(currency.uppercase()))
|
||||
throw Exception("There is no such currency as $currency")
|
||||
if (startDate == null || endDate == null) throw Exception("There is no start or end date")
|
||||
val trip = Trip(
|
||||
name = filename.substringBefore(".csv"),
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
currency = currency
|
||||
)
|
||||
val tripId = tripRepo.save(trip)
|
||||
|
||||
records.forEach {
|
||||
val dateTime = LocalDateTime.parse(it.get("date").substringBefore(","))
|
||||
val amount = it.get("amount")
|
||||
val currency = it.get("currency")
|
||||
val categoryName = it.get("category")
|
||||
val note = it.get("note")
|
||||
val category = categoryRepository.getByName(categoryName).first()
|
||||
val categoryId = category?.id
|
||||
?: categoryRepository.save(
|
||||
Category(
|
||||
name = categoryName,
|
||||
icon = Icons.entries.random(),
|
||||
color = colors.random()
|
||||
)
|
||||
).toInt()
|
||||
|
||||
val rate = exchangeRateRepository.getRate(
|
||||
Currencies.valueOf(currency),
|
||||
Currencies.valueOf(trip.currency),
|
||||
dateTime.toLocalDate(),
|
||||
)
|
||||
val expense = Expense(
|
||||
amount = amount.toDouble(),
|
||||
currency = currency,
|
||||
note = note,
|
||||
datetime = dateTime,
|
||||
categoryId = categoryId,
|
||||
tripId = tripId.toInt(),
|
||||
)
|
||||
expenseRepository.save(expense.copy(rate = rate))
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
} catch (ex: Exception) {
|
||||
onError(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ enum class Currencies {
|
||||
DZD,
|
||||
EGP,
|
||||
ERN,
|
||||
EUR,
|
||||
ETB,
|
||||
FJD,
|
||||
FKP,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
@@ -19,6 +17,7 @@ import cc.n0th1ng.tripmoney.data.repository.CategoryRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.ExchangeRateRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.ExpenseRepository
|
||||
import cc.n0th1ng.tripmoney.data.repository.TripRepository
|
||||
import cc.n0th1ng.tripmoney.service.ImportService
|
||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -38,7 +37,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
private val expenseRepo: ExpenseRepository,
|
||||
private val categoryRepo: CategoryRepository,
|
||||
private val exchangeRateRepository: ExchangeRateRepository,
|
||||
private val tripRepo: TripRepository
|
||||
private val tripRepo: TripRepository,
|
||||
private val importService: ImportService
|
||||
) : ViewModel() {
|
||||
|
||||
fun getBudgetLeft(tripId: Int): Flow<Double?> {
|
||||
@@ -52,7 +52,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
): Flow<PagingData<ExpenseDto>> =
|
||||
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getExpensesWithHeadersPaged(
|
||||
tripId: Int,
|
||||
search: String = "",
|
||||
@@ -136,14 +135,16 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
file.writer().use { writer ->
|
||||
CSVPrinter(
|
||||
writer,
|
||||
CSVFormat.DEFAULT.withHeader("date", "category", "currency", "amount")
|
||||
CSVFormat.DEFAULT.builder()
|
||||
.setHeader("date", "category", "currency", "amount", "note").get()
|
||||
).use { printer ->
|
||||
expenseRepo.getExpensesDto(tripId).first().forEach { expenseDto ->
|
||||
printer.printRecord(
|
||||
expenseDto.expense.datetime,
|
||||
expenseDto.category.name,
|
||||
expenseDto.expense.currency,
|
||||
expenseDto.expense.amount
|
||||
expenseDto.expense.amount,
|
||||
expenseDto.expense.note
|
||||
)
|
||||
|
||||
}
|
||||
@@ -214,6 +215,12 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun importCSV(csv: String, filename: String, onError: (Exception) -> Unit, onSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
importService.importCSV(csv, filename, onError, onSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearOldRates() {
|
||||
viewModelScope.launch {
|
||||
exchangeRateRepository.clearOldRates()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
@Singleton
|
||||
class SnackbarManager @Inject constructor() {
|
||||
private val _messages = MutableSharedFlow<String>()
|
||||
val messages = _messages.asSharedFlow()
|
||||
|
||||
suspend fun showMessage(message: String) {
|
||||
_messages.emit(message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cc.n0th1ng.tripmoney.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class SnackbarViewModel @Inject constructor(
|
||||
val snackbarManager: SnackbarManager
|
||||
) : ViewModel() {
|
||||
fun showMessage(message: String) {
|
||||
viewModelScope.launch {
|
||||
snackbarManager.showMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,4 +45,9 @@
|
||||
<string name="no_trip_picked">Wybierz wycieczkę żeby zobaczyć wydatki</string>
|
||||
<string name="no_trip_added">Zacznij budżetowanie od dodania wycieczki</string>
|
||||
<string name="no_expenses_summary">Brak wydatków do podsumowania</string>
|
||||
<string name="import_csv">Import z CSV</string>
|
||||
<string name="import_csv_subtext">Stwórz nową wycieczkę z CSV</string>
|
||||
<string name="import_export">Import/Eksport</string>
|
||||
<string name="went_wrong">Coś poszło nie tak.</string>
|
||||
<string name="import_success">Dane zaimportowane poprawnie!</string>
|
||||
</resources>
|
||||
@@ -45,4 +45,9 @@
|
||||
<string name="no_trip_picked">Select trip to see expenses</string>
|
||||
<string name="no_trip_added">Start budgeting by adding your trip</string>
|
||||
<string name="no_expenses_summary">No expenses to summarize</string>
|
||||
<string name="import_csv">import from CSV</string>
|
||||
<string name="import_csv_subtext">Create new trip from CSV</string>
|
||||
<string name="import_export">Import/Export</string>
|
||||
<string name="went_wrong">Sorry, something went wrong.</string>
|
||||
<string name="import_success">Data imported successfully!</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user