diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index ac4c1ba..201415e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -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 diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt index fe61eb3..8101080 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/TripDatabase.kt @@ -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) { diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/CategoryDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/CategoryDao.kt index 613c883..b2d03f9 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/CategoryDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/CategoryDao.kt @@ -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 @Delete suspend fun delete(category: Category) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt index 3c95cf7..7e71b7e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/TripDao.kt @@ -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( """ diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/CategoryRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/CategoryRepository.kt index 785175e..ca11878 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/CategoryRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/CategoryRepository.kt @@ -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 { + return categoryDao.getByName(name) } @WorkerThread diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt index 82f16d2..439d04d 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExchangeRateRepository.kt @@ -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) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt index 1ff0370..d8234e4 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/TripRepository.kt @@ -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> { diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt index 2a0b768..fafb0f8 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/addexpense/AddExpenseBottomSheet.kt @@ -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 diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt index 0c9bdd0..a124994 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt @@ -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, diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt index c3fc911..71dfa01 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/settings/SettingsScreen.kt @@ -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(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(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 ) } - SettingsListItem( - onClick = onExportToCsv, - stringResource(string.export_to_csv), - supportingText = stringResource(string.export_csv_subttext).format(tripName), - iconResource = R.drawable.materialsymbols_ic_csv_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 diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/service/ImportService.kt b/app/src/main/java/cc/n0th1ng/tripmoney/service/ImportService.kt new file mode 100644 index 0000000..a25feb9 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/service/ImportService.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt index c205304..2f63ca1 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/utils/Currencies.kt @@ -42,6 +42,7 @@ enum class Currencies { DZD, EGP, ERN, + EUR, ETB, FJD, FKP, diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt index ade69e3..477486a 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -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 { @@ -52,7 +52,6 @@ open class ExpenseAndCategoryViewModel @Inject constructor( ): Flow> = 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() diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarManager.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarManager.kt new file mode 100644 index 0000000..6de7c6d --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarManager.kt @@ -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() + val messages = _messages.asSharedFlow() + + suspend fun showMessage(message: String) { + _messages.emit(message) + } +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarViewModel.kt new file mode 100644 index 0000000..e478472 --- /dev/null +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/SnackbarViewModel.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 998995d..7032a74 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -45,4 +45,9 @@ Wybierz wycieczkę żeby zobaczyć wydatki Zacznij budżetowanie od dodania wycieczki Brak wydatków do podsumowania + Import z CSV + Stwórz nową wycieczkę z CSV + Import/Eksport + Coś poszło nie tak. + Dane zaimportowane poprawnie! \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5c914a..5657703 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,4 +45,9 @@ Select trip to see expenses Start budgeting by adding your trip No expenses to summarize + import from CSV + Create new trip from CSV + Import/Export + Sorry, something went wrong. + Data imported successfully! \ No newline at end of file