Compare commits

...

5 Commits

Author SHA1 Message Date
69e0344261 Implemented expense creation functionality.
A new `ExpenseCreationScreen` and `ExpenseCreationViewModel` have been added to allow users to create new expenses. This includes fields for the title, amount, and a participant selection using filter chips.

Key changes:
- Added `ExpenseCreationViewModel` to handle user loading and persistent storage of expenses and shares.
- Created `ExpenseCreationScreen` UI with validation for the "Save" button.
- Updated `DashboardScreen` to include a Floating Action Button (FAB) for navigating to the expense creation flow.
- Added `expenseCreate` API endpoint and related data types (`ExpenseCreationRequest`, `ExpenseCreationResponse`).
- Integrated `ExpenseCreationViewModel` into `MainActivity` and `AppContent`.
2026-03-04 14:55:31 +01:00
25d1038c9d Updated UI text to English and improved currency formatting
The UI strings in `ExpenseDetailScreen`, `DashboardScreen`, and `AccountSelectionScreen` have been translated from German to English.

Additionally, currency display logic has been updated to use `String.format("%.2f€", ...)` for consistent two-decimal formatting across the `ExpenseDetailScreen` and `DashboardScreen`.
2026-03-04 14:22:21 +01:00
ea6349c85c Added Expense Detail screen and functionality
The new `ExpenseDetailScreen` displays expense information, including title, amount, and a breakdown of cost shares among users.

Key changes:
- Created `ExpenseDetailViewModel` to manage loading and combining expense shares with user information.
- Added `ExpenseDetailScreen` UI with a list of shares and a back navigation option.
- Updated `MainActivity` and `AppContent` to handle navigation to the expense detail view.
- Extended `ExpenseShareDao` and `ExpenseShareRepository` to support fetching shares by expense ID.
- Updated `APIService` and `UserRepository` to simplify user information retrieval and support querying shares by different ID types.
- Added `@OptIn(ExperimentalMaterial3Api::class)` to several UI components to support Material 3 features.
2026-03-04 14:16:45 +01:00
4104930ea5 Added UserRepository and ExpenseShareRepository
Implemented `UserRepository` and `ExpenseShareRepository` to handle data fetching with a caching strategy (local DAO + remote API).

Specific changes include:
- Added `getUserById` to `UserDao` and updated `getShareById` in `ExpenseShareDao` to support nullable returns.
- Updated `APIService` and `ComDataTypes` to include endpoints and data models for User info, Expense Shares, and pluralized Expense responses.
- Refactored `ExpenseRepository` to use the updated API naming conventions and removed debug print statements.
2026-03-04 12:28:12 +01:00
3d789c0352 Implemented a repository-based data layer for expenses and updated Room entities.
The `ExpenseRepository` was introduced to handle data fetching with a caching strategy, utilizing a new `Resource` sealed class to represent Loading, Success, and Error states.

Key changes include:
- Added `ExpenseRepository` to manage data flow between the local database and remote API.
- Updated `MainViewModel` to use the repository and expose expenses via a `StateFlow<Resource<List<Expense>>>`.
- Enhanced Room entities (`Expense`, `ExpenseShare`, `User`) with `@PrimaryKey` annotations and added a `Converters` class to handle `List<String>` types.
- Expanded `AppDatabase` to include DAOs for `Expense`, `ExpenseShare`, and `User`.
- Updated `DashboardScreen` to reactively display loading indicators, error messages, and cached/remote expense data.
2026-03-04 11:56:57 +01:00
20 changed files with 687 additions and 85 deletions

View File

@@ -8,11 +8,14 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
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 de.miaurizius.shap_planner.TokenStorage import de.miaurizius.shap_planner.TokenStorage
import de.miaurizius.shap_planner.UserPreferences import de.miaurizius.shap_planner.UserPreferences
import de.miaurizius.shap_planner.room.AppDatabase import de.miaurizius.shap_planner.room.AppDatabase
import de.miaurizius.shap_planner.ui.AppContent import de.miaurizius.shap_planner.ui.AppContent
import de.miaurizius.shap_planner.ui.theme.ShapPlannerTheme import de.miaurizius.shap_planner.ui.theme.ShapPlannerTheme
import de.miaurizius.shap_planner.viewmodels.ExpenseCreationViewModel
import de.miaurizius.shap_planner.viewmodels.ExpenseDetailViewModel
import de.miaurizius.shap_planner.viewmodels.LoginViewModel import de.miaurizius.shap_planner.viewmodels.LoginViewModel
import de.miaurizius.shap_planner.viewmodels.MainViewModel import de.miaurizius.shap_planner.viewmodels.MainViewModel
@@ -23,9 +26,26 @@ class MainActivity : ComponentActivity() {
val prefs = UserPreferences(this) val prefs = UserPreferences(this)
val loginViewModel = LoginViewModel(prefs, applicationContext) val loginViewModel = LoginViewModel(prefs, applicationContext)
val database = AppDatabase.getDatabase(applicationContext) val database = AppDatabase.getDatabase(applicationContext)
val dao = database.accountDao() val accountDao = database.accountDao()
val expenseDao = database.expenseDao()
val tokenStorage = TokenStorage(applicationContext) val tokenStorage = TokenStorage(applicationContext)
val mainViewModel = MainViewModel(dao, tokenStorage) val mainViewModel = MainViewModel(
accountDao,
expenseDao,
tokenStorage
)
val detailViewModel = ExpenseDetailViewModel(
database.expenseDao(),
database.expenseShareDao(),
database.userDao(),
tokenStorage
)
val creationViewModel = ExpenseCreationViewModel(
database.userDao(),
database.expenseDao(),
database.expenseShareDao(),
tokenStorage
)
setContent { setContent {
@@ -33,7 +53,6 @@ class MainActivity : ComponentActivity() {
val accountList by mainViewModel.accounts.collectAsState() val accountList by mainViewModel.accounts.collectAsState()
val selectedAccount = mainViewModel.selectedAccount val selectedAccount = mainViewModel.selectedAccount
val showLoginForNewAccount = remember { mutableStateOf(false) } val showLoginForNewAccount = remember { mutableStateOf(false) }
val expenses by mainViewModel.expenses.collectAsState()
BackHandler(enabled = showLoginForNewAccount.value && accountList.isNotEmpty()) { BackHandler(enabled = showLoginForNewAccount.value && accountList.isNotEmpty()) {
showLoginForNewAccount.value = false showLoginForNewAccount.value = false
@@ -54,9 +73,10 @@ class MainActivity : ComponentActivity() {
sessionState = mainViewModel.sessionState, sessionState = mainViewModel.sessionState,
onValidateSession = { mainViewModel.validateSession(selectedAccount!!) }, onValidateSession = { mainViewModel.validateSession(selectedAccount!!) },
onSessionInvalid = { mainViewModel.logoutFromAccount() }, onSessionInvalid = { mainViewModel.logoutFromAccount() },
expenses = expenses,
onExpenseClick = { expense -> println("Clicked: ${expense.title}") }, onExpenseClick = { expense -> println("Clicked: ${expense.title}") },
viewModel = mainViewModel viewModel = mainViewModel,
detailViewModel = detailViewModel,
creationViewModel = creationViewModel,
) )
} }
} }

View File

@@ -5,21 +5,22 @@ import androidx.room.Delete
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.UUID import java.util.UUID
@Entity(tableName = "expenses") @Entity(tableName = "expenses")
data class Expense ( data class Expense (
val id: UUID, @PrimaryKey val id: UUID,
val payer_id: UUID, val payer_id: UUID,
val amount: Int, val amount: Int,
val title: String, val title: String,
val description: String, val description: String,
val attachments: List<String>, val attachments: List<String>?,
val created_at: Int, val created_at: Int,
val last_updated_at: Int val last_updated_at: Int
) )
@Dao @Dao
interface ExpenseDao { interface ExpenseDao {

View File

@@ -5,13 +5,14 @@ import androidx.room.Delete
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.UUID import java.util.UUID
@Entity(tableName = "expense_shares") @Entity(tableName = "expense_shares")
data class ExpenseShare( data class ExpenseShare(
val id: UUID, @PrimaryKey val id: UUID,
val expense_id: UUID, val expense_id: UUID,
val user_id: UUID, val user_id: UUID,
val share_cents: Int val share_cents: Int
@@ -23,7 +24,10 @@ interface ExpenseShareDao {
fun getAllShares(): Flow<List<ExpenseShare>> fun getAllShares(): Flow<List<ExpenseShare>>
@Query("SELECT * FROM expense_shares WHERE id = :shareId") @Query("SELECT * FROM expense_shares WHERE id = :shareId")
fun getShareById(shareId: UUID) fun getShareById(shareId: UUID): Flow<ExpenseShare?>
@Query("SELECT * FROM expense_shares WHERE expense_id = :expense_id")
fun getSharesByExpense(expense_id: UUID): Flow<List<ExpenseShare>>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertShare(share: ExpenseShare) suspend fun insertShare(share: ExpenseShare)

View File

@@ -5,13 +5,14 @@ import androidx.room.Delete
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query import androidx.room.Query
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.UUID import java.util.UUID
@Entity(tableName = "users") @Entity(tableName = "users")
data class User ( data class User (
val id: UUID, @PrimaryKey val id: UUID,
val name: String, val name: String,
val avatar_url: String? val avatar_url: String?
) )
@@ -21,6 +22,9 @@ interface UserDao {
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>> fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: UUID): Flow<User?>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User) suspend fun insertUser(user: User)

View File

@@ -1,5 +1,7 @@
package de.miaurizius.shap_planner.network package de.miaurizius.shap_planner.network
import com.google.gson.annotations.SerializedName
import de.miaurizius.shap_planner.entities.User
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE import retrofit2.http.DELETE
@@ -21,15 +23,32 @@ interface APIService {
// Expenses // Expenses
@GET("api/expenses") @GET("api/expenses")
suspend fun expenseGet(@Header("Authorization") token: String): Response<ExpenseResponse> suspend fun expensesGet(@Header("Authorization") token: String): Response<ExpensesResponse>
@POST("api/expenses") @POST("api/expenses")
suspend fun expenseCreate(@Header("Authorization") token: String) suspend fun expenseCreate(@Header("Authorization") token: String, @Body req: ExpenseCreationRequest): Response<ExpenseCreationResponse>
@PUT("api/expenses") @PUT("api/expenses")
suspend fun expenseUpdate(@Header("Authorization") token: String) suspend fun expenseUpdate(@Header("Authorization") token: String)
@DELETE("api/expenses") @DELETE("api/expenses")
suspend fun expenseDelete(@Header("Authorization") token: String) suspend fun expenseDelete(@Header("Authorization") token: String)
// Shares
@GET("api/shares")
suspend fun sharesGet(@Header("Authorization") token: String): Response<ExpenseSharesResponse>
@GET("api/shares")
suspend fun shareGet(@Header("Authorization") token: String, @Query("id") shareId: UUID): Response<ExpenseShareResponse>
@GET("api/shares")
suspend fun shareGet(@Header("Authorization") token: String, @Query("id") expenseId: UUID, @Query("idType") idType: IDType): Response<ExpenseSharesResponse>
// User // User
@GET("api/userinfo") @GET("api/userinfo")
suspend fun userinfo(@Header("Authorization") token: String, @Query("id") userId: UUID) suspend fun userinfo(@Header("Authorization") token: String, @Query("id") userId: UUID): Response<User>
}
enum class IDType {
@SerializedName("share")
Share,
@SerializedName("expense")
Expense,
@SerializedName("user")
User,
} }

View File

@@ -1,6 +1,7 @@
package de.miaurizius.shap_planner.network package de.miaurizius.shap_planner.network
import de.miaurizius.shap_planner.entities.Expense import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseShare
// Login // Login
data class LoginRequest(val username: String, val password: String) data class LoginRequest(val username: String, val password: String)
@@ -12,4 +13,10 @@ data class RefreshRequest(val refresh_token: String)
data class RefreshResponse(val access_token: String, val refresh_token: String) data class RefreshResponse(val access_token: String, val refresh_token: String)
// Expenses // Expenses
data class ExpenseResponse(val expenses: List<Expense>) data class ExpensesResponse(val expenses: List<Expense>)
data class ExpenseCreationRequest(val expense: Expense, val shares: List<ExpenseShare>)
data class ExpenseCreationResponse(val expense: Expense, val shares: List<ExpenseShare>)
// ExpenseShares
data class ExpenseSharesResponse(val shares: List<ExpenseShare>)
data class ExpenseShareResponse(val share: ExpenseShare)

View File

@@ -0,0 +1,33 @@
package de.miaurizius.shap_planner.repository
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseDao
import de.miaurizius.shap_planner.network.APIService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
class ExpenseRepository(
private val dao: ExpenseDao,
private val api: APIService
) {
fun getExpenses(token: String, forceRefresh: Boolean = false): Flow<Resource<List<Expense>>> = flow {
val cachedExpense = dao.getAllExpenses().first()
emit(Resource.Loading(cachedExpense))
if(cachedExpense.isEmpty() || forceRefresh) {
try {
val response = api.expensesGet("Bearer $token")
if(response.isSuccessful) {
val remoteExpense = response.body()?.expenses ?: emptyList()
remoteExpense.forEach {
dao.insertExpense(it)
}
}
} catch(e: Exception) {
emit(Resource.Error("Network Error: ${e.localizedMessage}", cachedExpense))
}
}
dao.getAllExpenses().collect { emit(Resource.Success(it)) }
}
}

View File

@@ -0,0 +1,73 @@
package de.miaurizius.shap_planner.repository
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseShare
import de.miaurizius.shap_planner.entities.ExpenseShareDao
import de.miaurizius.shap_planner.network.APIService
import de.miaurizius.shap_planner.network.IDType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import java.util.UUID
class ExpenseShareRepository(
private val dao: ExpenseShareDao,
private val api: APIService
) {
fun getShares(token: String, forceRefresh: Boolean = false): Flow<Resource<List<ExpenseShare>>> = flow {
val cachedData = dao.getAllShares().first()
emit(Resource.Loading(cachedData))
if(cachedData.isEmpty() || forceRefresh) {
try {
val response = api.sharesGet("Bearer $token")
if(response.isSuccessful) {
val remoteShare = response.body()?.shares ?: emptyList()
remoteShare.forEach {
dao.insertShare(it)
}
}
} catch(e: Exception) {
emit(Resource.Error("Network Error: ${e.localizedMessage}", cachedData))
}
}
dao.getAllShares().collect { emit(Resource.Success(it)) }
}
fun getShareById(token: String, shareId: UUID, forceRefresh: Boolean = false): Flow<Resource<ExpenseShare>> = flow {
val cached = dao.getShareById(shareId).first()
emit(Resource.Loading(cached))
if(cached == null || forceRefresh) {
try {
val response = api.shareGet("Bearer $token", shareId)
if(response.isSuccessful) {
response.body()?.share?.let { remoteShare -> dao.insertShare(remoteShare) }
}
} catch(e: Exception) {
emit(Resource.Error("Network-Error: ${e.localizedMessage}", cached))
}
}
dao.getShareById(shareId).collect { share ->
if(share != null) emit(Resource.Success(share))
else emit(Resource.Error("Share nicht gefunden", null))
}
}
fun getSharesByExpenseId(token: String, expenseId: UUID, forceRefresh: Boolean = false): Flow<Resource<List<ExpenseShare>>> = flow {
val cached = dao.getSharesByExpense(expenseId).first()
emit(Resource.Loading(cached))
if(cached.isEmpty() || forceRefresh) {
try {
val response = api.shareGet("Bearer $token", expenseId, IDType.Expense)
if(response.isSuccessful) {
println("Body: ${response.body()}")
val remoteShare = response.body()?.shares ?: emptyList()
remoteShare.forEach { dao.insertShare(it) }
}
} catch(e: Exception) {
emit(Resource.Error("Network Error: ${e.localizedMessage}", cached))
}
}
dao.getSharesByExpense(expenseId).collect { emit(Resource.Success(it)) }
}
}

View File

@@ -0,0 +1,7 @@
package de.miaurizius.shap_planner.repository
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T): Resource<T>(data)
class Loading<T>(data: T? = null): Resource<T>(data)
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
}

View File

@@ -0,0 +1,32 @@
package de.miaurizius.shap_planner.repository
import de.miaurizius.shap_planner.entities.User
import de.miaurizius.shap_planner.entities.UserDao
import de.miaurizius.shap_planner.network.APIService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import java.util.UUID
class UserRepository(
private val dao: UserDao,
private val api: APIService
) {
fun getUser(token: String, userId: UUID, forceRefresh: Boolean = false): Flow<Resource<User>> = flow {
val cached = dao.getUserById(userId).first()
emit(Resource.Loading(cached))
if(cached == null || forceRefresh) {
try {
val response = api.userinfo("Bearer $token", userId)
if(response.isSuccessful) {
println("Body: ${response.body()}")
response.body()?.let { remoteUser -> dao.insertUser(remoteUser) }
}
} catch(e: Exception) {
emit(Resource.Error("Network-Error: ${e.localizedMessage}", cached))
}
}
dao.getUserById(userId).collect { user -> if(user != null) emit(Resource.Success(user)) else emit(
Resource.Error("User nicht gefunden", null)) }
}
}

View File

@@ -4,12 +4,31 @@ import android.content.Context
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.TypeConverter
import androidx.room.TypeConverters
import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.AccountDao import de.miaurizius.shap_planner.entities.AccountDao
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseDao
import de.miaurizius.shap_planner.entities.ExpenseShare
import de.miaurizius.shap_planner.entities.ExpenseShareDao
import de.miaurizius.shap_planner.entities.User
import de.miaurizius.shap_planner.entities.UserDao
@Database(entities = [Account::class], version = 4) class Converters {
@TypeConverter
fun fromList(list: List<String>?): String? = list?.joinToString(",")
@TypeConverter
fun toList(data: String?): List<String>? = data?.split(",")?.map { it.trim() }
}
@Database(entities = [Account::class, Expense::class, ExpenseShare::class, User::class], version = 6)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun accountDao(): AccountDao abstract fun accountDao(): AccountDao
abstract fun expenseDao(): ExpenseDao
abstract fun expenseShareDao(): ExpenseShareDao
abstract fun userDao(): UserDao
companion object { companion object {
@Volatile @Volatile
private var INSTANCE: AppDatabase? = null private var INSTANCE: AppDatabase? = null

View File

@@ -1,12 +1,22 @@
package de.miaurizius.shap_planner.ui package de.miaurizius.shap_planner.ui
import androidx.activity.compose.BackHandler
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.Expense import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.network.SessionState import de.miaurizius.shap_planner.network.SessionState
import de.miaurizius.shap_planner.ui.screens.AccountSelectionScreen import de.miaurizius.shap_planner.ui.screens.AccountSelectionScreen
import de.miaurizius.shap_planner.ui.screens.DashboardScreen import de.miaurizius.shap_planner.ui.screens.DashboardScreen
import de.miaurizius.shap_planner.ui.screens.ExpenseCreationScreen
import de.miaurizius.shap_planner.ui.screens.ExpenseDetailScreen
import de.miaurizius.shap_planner.ui.screens.LoginScreen import de.miaurizius.shap_planner.ui.screens.LoginScreen
import de.miaurizius.shap_planner.viewmodels.ExpenseCreationViewModel
import de.miaurizius.shap_planner.viewmodels.ExpenseDetailViewModel
import de.miaurizius.shap_planner.viewmodels.MainViewModel import de.miaurizius.shap_planner.viewmodels.MainViewModel
@Composable @Composable
@@ -18,7 +28,6 @@ fun AppContent(
onLogin: (String, String, String) -> Unit, onLogin: (String, String, String) -> Unit,
// Expenses // Expenses
expenses: List<Expense>,
onExpenseClick: (Expense) -> Unit, onExpenseClick: (Expense) -> Unit,
// Account // Account
@@ -33,16 +42,36 @@ fun AppContent(
onSessionInvalid: () -> Unit, onSessionInvalid: () -> Unit,
//Important //Important
viewModel: MainViewModel viewModel: MainViewModel,
detailViewModel: ExpenseDetailViewModel,
creationViewModel: ExpenseCreationViewModel
) { ) {
var selectedExpense by remember { mutableStateOf<Expense?>(null) }
var showAddExpenseScreen by remember { mutableStateOf(false) }
when { when {
showAddExpenseScreen -> {
ExpenseCreationScreen(
account = selectedAccount!!,
viewModel = creationViewModel,
onBack = { showAddExpenseScreen = false },
onSaved = { showAddExpenseScreen = false }
)
}
selectedExpense != null -> {
ExpenseDetailScreen(
expense = selectedExpense!!,
account = selectedAccount!!,
viewModel = detailViewModel,
onBack = { selectedExpense = null }
)
}
showLoginForNewAccount -> LoginScreen(onLogin) showLoginForNewAccount -> LoginScreen(onLogin)
accountList.isEmpty() -> LoginScreen(onLogin) accountList.isEmpty() -> LoginScreen(onLogin)
selectedAccount != null -> DashboardScreen( selectedAccount != null -> DashboardScreen(
// Data and regarding Methods // Data and regarding Methods
account = selectedAccount, account = selectedAccount,
expenses = expenses, onExpenseClick = { selectedExpense = it },
onExpenseClick = onExpenseClick,
// Default Methods // Default Methods
mainViewModel = viewModel, mainViewModel = viewModel,
@@ -50,7 +79,8 @@ fun AppContent(
onDelete = onDeleteAccount, onDelete = onDeleteAccount,
sessionState = sessionState, sessionState = sessionState,
onValidate = onValidateSession, onValidate = onValidateSession,
onSessionInvalid = onSessionInvalid onSessionInvalid = onSessionInvalid,
onAddExpenseClick = { showAddExpenseScreen = true },
) )
else -> AccountSelectionScreen( else -> AccountSelectionScreen(
accounts = accountList, accounts = accountList,

View File

@@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -30,6 +31,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.entities.Account
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccountSelectionScreen(accounts: List<Account>, onAccountClick: (Account) -> Unit, onAddAccountClick: () -> Unit) { fun AccountSelectionScreen(accounts: List<Account>, onAccountClick: (Account) -> Unit, onAddAccountClick: () -> Unit) {
LazyColumn( LazyColumn(
@@ -41,7 +43,7 @@ fun AccountSelectionScreen(accounts: List<Account>, onAccountClick: (Account) ->
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
item { item {
Text("Wähle einen Account", style = MaterialTheme.typography.headlineSmall) Text("Choose an account", style = MaterialTheme.typography.headlineSmall)
} }
items(accounts) { account -> items(accounts) { account ->
@@ -62,7 +64,7 @@ fun AccountSelectionScreen(accounts: List<Account>, onAccountClick: (Account) ->
onClick = onAddAccountClick, onClick = onAddAccountClick,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text("Anderen Account hinzufügen") Text("Add account")
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package de.miaurizius.shap_planner.ui.screens package de.miaurizius.shap_planner.ui.screens
import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -19,11 +20,15 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -32,14 +37,16 @@ import androidx.compose.ui.unit.dp
import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.Expense import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.network.SessionState import de.miaurizius.shap_planner.network.SessionState
import de.miaurizius.shap_planner.repository.Resource
import de.miaurizius.shap_planner.viewmodels.MainViewModel import de.miaurizius.shap_planner.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
// Data and regarding Methods // Data and regarding Methods
account: Account, account: Account,
expenses: List<Expense>,
onExpenseClick: (Expense) -> Unit, onExpenseClick: (Expense) -> Unit,
onAddExpenseClick: () -> Unit,
// Default Methods // Default Methods
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
@@ -49,8 +56,10 @@ fun DashboardScreen(
onValidate: () -> Unit, onValidate: () -> Unit,
onSessionInvalid: () -> Unit) { onSessionInvalid: () -> Unit) {
val expenseResource by mainViewModel.expenseResource.collectAsState()
LaunchedEffect(Unit) { onValidate() } LaunchedEffect(Unit) { onValidate() }
mainViewModel.loadExpenses(account) LaunchedEffect(account) { mainViewModel.loadExpenses(account, forceRefresh = false) }
when (sessionState) { when (sessionState) {
SessionState.Loading -> { SessionState.Loading -> {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -62,9 +71,19 @@ fun DashboardScreen(
BackHandler { BackHandler {
onBack() onBack()
} }
Scaffold(floatingActionButton = {
androidx.compose.material3.FloatingActionButton(
onClick = onAddExpenseClick,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) {
Text("+", style = MaterialTheme.typography.headlineSmall)
}
}) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)
.padding(16.dp) .padding(16.dp)
.statusBarsPadding() .statusBarsPadding()
.navigationBarsPadding() .navigationBarsPadding()
@@ -77,31 +96,39 @@ fun DashboardScreen(
) { ) {
Column { Column {
Text( Text(
text = "Hallo, ${account.name}!", text = "Hello, ${account.name}!",
style = MaterialTheme.typography.headlineMedium style = MaterialTheme.typography.headlineMedium
) )
Text( Text(
text = "WG: ${account.wgName}", text = "Household: ${account.wgName}",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = Color.Gray color = Color.Gray
) )
} }
Button(onClick = onBack) { Button(onClick = onBack) {
Text("Wechseln") Text("Switch")
} }
} }
Spacer(modifier = Modifier.height(5.dp)) Spacer(modifier = Modifier.height(5.dp))
Button(onClick = onDelete) { Button(onClick = onDelete) {
Text("Löschen") Text("Delete")
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
Text("WG-Kosten", style = MaterialTheme.typography.titleLarge) Text("Costs", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
if(expenseResource is Resource.Loading && expenseResource.data?.isEmpty() == true) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
}
if(expenseResource is Resource.Error) {
Text("Error: ${expenseResource.message}", color = Color.Red)
}
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -109,12 +136,13 @@ fun DashboardScreen(
contentPadding = PaddingValues(8.dp), contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(expenses) { expense -> items(expenseResource.data ?: emptyList()) { expense ->
ExpenseItem(expense = expense, onClick = { onExpenseClick(expense) }) ExpenseItem(expense = expense, onClick = { onExpenseClick(expense) })
} }
} }
} }
} }
}
SessionState.Invalid -> { SessionState.Invalid -> {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -128,6 +156,7 @@ fun DashboardScreen(
} }
} }
@SuppressLint("DefaultLocale")
@Composable @Composable
fun ExpenseItem(expense: Expense, onClick: () -> Unit) { fun ExpenseItem(expense: Expense, onClick: () -> Unit) {
Surface(modifier = Modifier Surface(modifier = Modifier
@@ -137,7 +166,7 @@ fun ExpenseItem(expense: Expense, onClick: () -> Unit) {
color = MaterialTheme.colorScheme.surface) { color = MaterialTheme.colorScheme.surface) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) { Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Text(text = expense.title, style = MaterialTheme.typography.bodyLarge) Text(text = expense.title, style = MaterialTheme.typography.bodyLarge)
Text(text = expense.amount.toString()+"", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold) Text(text = String.format("%.2f€", expense.amount / 100.0), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
} }
} }
} }

View File

@@ -0,0 +1,87 @@
package de.miaurizius.shap_planner.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.viewmodels.ExpenseCreationViewModel
import java.util.UUID
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ExpenseCreationScreen(
account: Account,
viewModel: ExpenseCreationViewModel,
onSaved: () -> Unit,
onBack: () -> Unit
) {
var title by remember { mutableStateOf("") }
var amountStr by remember { mutableStateOf("") }
val users by viewModel.users.collectAsState()
val selectedUsers = remember { mutableStateListOf<UUID>() }
LaunchedEffect(Unit) { viewModel.loadUsers() }
Scaffold(
topBar = {
TopAppBar(title = { Text("New Expense") })
}
) { padding ->
Column(modifier = Modifier.padding(padding).padding(16.dp)) {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("What was bought?") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = amountStr,
onValueChange = { amountStr = it },
label = { Text("Amount in €") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Text("Who participated?", style = MaterialTheme.typography.titleMedium)
FlowRow(modifier = Modifier.padding(vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
users.forEach { user ->
FilterChip(
selected = selectedUsers.contains(user.id),
onClick = {
if (selectedUsers.contains(user.id)) selectedUsers.remove(user.id)
else selectedUsers.add(user.id)
},
label = { Text(user.name) }
)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
val cents = (amountStr.replace(",", ".").toDoubleOrNull() ?: 0.0) * 100
viewModel.saveExpense(account, title, cents.toInt(), selectedUsers.toList())
onSaved()
},
modifier = Modifier.fillMaxWidth(),
enabled = title.isNotBlank() && amountStr.isNotBlank() && selectedUsers.isNotEmpty()
) {
Text("Save")
}
TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) {
Text("Cancel")
}
}
}
}

View File

@@ -0,0 +1,109 @@
package de.miaurizius.shap_planner.ui.screens
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.viewmodels.ExpenseDetailViewModel
@SuppressLint("DefaultLocale")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExpenseDetailScreen(
expense: Expense,
account: Account,
viewModel: ExpenseDetailViewModel,
onBack: () -> Unit
) {
val shares by viewModel.sharesWithUser.collectAsState()
LaunchedEffect(expense) {
viewModel.loadExpenseDetail(account, expense)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(expense.title) },
navigationIcon = { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
} }
)
}
) { padding ->
Column(modifier = Modifier.padding(padding).padding(16.dp)) {
Text(
text = String.format("%.2f€", expense.amount / 100.0),
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(expense.description, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
Spacer(modifier = Modifier.height(16.dp))
Text("Cost Allocation", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(shares) { item ->
ShareItem(item.user?.name ?: "Unknown User", item.share.share_cents)
}
}
}
}
}
@SuppressLint("DefaultLocale")
@Composable
fun ShareItem(name: String, amountCents: Int) {
Card(modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(name, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f))
Text(
text = String.format("%.2f€", amountCents / 100.0),
color = if (amountCents > 0) Color(0xFF4CAF50) else Color.Gray,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge
)
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -18,6 +19,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LoginScreen(onLogin: (String, String, String) -> Unit, onBack: (() -> Unit)? = null) { fun LoginScreen(onLogin: (String, String, String) -> Unit, onBack: (() -> Unit)? = null) {

View File

@@ -0,0 +1,64 @@
package de.miaurizius.shap_planner.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.miaurizius.shap_planner.TokenStorage
import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseDao
import de.miaurizius.shap_planner.entities.ExpenseShare
import de.miaurizius.shap_planner.entities.ExpenseShareDao
import de.miaurizius.shap_planner.entities.User
import de.miaurizius.shap_planner.entities.UserDao
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.UUID
class ExpenseCreationViewModel(
private val userDao: UserDao,
private val expenseDao: ExpenseDao,
private val shareDao: ExpenseShareDao,
private val tokenStorage: TokenStorage
) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users
fun loadUsers() {
viewModelScope.launch {
userDao.getAllUsers().collect { _users.value = it }
}
}
fun saveExpense(account: Account, title: String, amountCents: Int, selectedUserIds: List<UUID>) {
viewModelScope.launch {
val expenseId = UUID.randomUUID() //TODO: Backend has to generate UUID
val newExpense = Expense(
id = expenseId,
payer_id = account.id,
amount = amountCents,
title = title,
description = "",
attachments = null,
created_at = (System.currentTimeMillis() / 1000).toInt(),
last_updated_at = 0
)
expenseDao.insertExpense(newExpense)
val shareAmount = amountCents / selectedUserIds.size
selectedUserIds.forEach { userId ->
shareDao.insertShare(
ExpenseShare(
UUID.randomUUID(), //TODO: Backend has to generate UUID
expenseId,
userId,
shareAmount
)
)
}
// API Calls
}
}
}

View File

@@ -0,0 +1,59 @@
package de.miaurizius.shap_planner.viewmodels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.miaurizius.shap_planner.TokenStorage
import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseDao
import de.miaurizius.shap_planner.entities.ExpenseShare
import de.miaurizius.shap_planner.entities.ExpenseShareDao
import de.miaurizius.shap_planner.entities.User
import de.miaurizius.shap_planner.entities.UserDao
import de.miaurizius.shap_planner.network.RetrofitProvider
import de.miaurizius.shap_planner.repository.ExpenseShareRepository
import de.miaurizius.shap_planner.repository.Resource
import de.miaurizius.shap_planner.repository.UserRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
data class ShareWithUser(
val share: ExpenseShare,
val user: User?
)
class ExpenseDetailViewModel(
private val expenseDao: ExpenseDao,
private val shareDao: ExpenseShareDao,
private val userDao: UserDao,
private val tokenStorage: TokenStorage
) : ViewModel() {
private val _sharesWithUser = MutableStateFlow<List<ShareWithUser>>(emptyList())
val sharesWithUser: StateFlow<List<ShareWithUser>> = _sharesWithUser
fun loadExpenseDetail(account: Account, expense: Expense) {
viewModelScope.launch {
val api = RetrofitProvider.create(account.serverUrl)
val token = tokenStorage.getAccess(account.id.toString()) ?: ""
val shareRepo = ExpenseShareRepository(shareDao, api)
val userRepo = UserRepository(userDao, api)
shareRepo.getSharesByExpenseId(token, expense.id).collect { resource ->
val shares = resource.data ?: emptyList()
val combinedList = shares.map { share ->
val cachedUser = userDao.getUserById(share.user_id).first()
if (cachedUser == null) {
val userResource = userRepo.getUser(token, share.user_id).first { it is Resource.Success || it is Resource.Error }
ShareWithUser(share, userResource.data)
} else {
ShareWithUser(share, cachedUser)
}
}
_sharesWithUser.value = combinedList
}
}
}
}

View File

@@ -9,9 +9,12 @@ import de.miaurizius.shap_planner.TokenStorage
import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.entities.Account
import de.miaurizius.shap_planner.entities.AccountDao import de.miaurizius.shap_planner.entities.AccountDao
import de.miaurizius.shap_planner.entities.Expense import de.miaurizius.shap_planner.entities.Expense
import de.miaurizius.shap_planner.entities.ExpenseDao
import de.miaurizius.shap_planner.network.RefreshRequest import de.miaurizius.shap_planner.network.RefreshRequest
import de.miaurizius.shap_planner.network.RetrofitProvider import de.miaurizius.shap_planner.network.RetrofitProvider
import de.miaurizius.shap_planner.network.SessionState import de.miaurizius.shap_planner.network.SessionState
import de.miaurizius.shap_planner.repository.ExpenseRepository
import de.miaurizius.shap_planner.repository.Resource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -19,7 +22,11 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.emptyList import kotlin.collections.emptyList
class MainViewModel(private val accountDao: AccountDao, private val tokenStorage: TokenStorage) : ViewModel() { class MainViewModel(
private val accountDao: AccountDao,
private val expenseDao: ExpenseDao,
private val tokenStorage: TokenStorage
) : ViewModel() {
var selectedAccount by mutableStateOf<Account?>(null) var selectedAccount by mutableStateOf<Account?>(null)
private set private set
@@ -28,22 +35,16 @@ class MainViewModel(private val accountDao: AccountDao, private val tokenStorage
var sessionState by mutableStateOf<SessionState>(SessionState.Loading) var sessionState by mutableStateOf<SessionState>(SessionState.Loading)
private set private set
private val _expenses = MutableStateFlow<List<Expense>>(emptyList()) private val _expenseResource = MutableStateFlow<Resource<List<Expense>>>(Resource.Loading(emptyList()))
val expenses: StateFlow<List<Expense>> = _expenses val expenseResource: StateFlow<Resource<List<Expense>>> = _expenseResource
fun loadExpenses(account: Account) { fun loadExpenses(account: Account, forceRefresh: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
try {
val api = RetrofitProvider.create(account.serverUrl) val api = RetrofitProvider.create(account.serverUrl)
val accessToken = tokenStorage.getAccess(account.id.toString()) val repo = ExpenseRepository(expenseDao, api)
val accessToken = tokenStorage.getAccess(account.id.toString()) ?: ""
val response = api.expenseGet("Bearer $accessToken") repo.getExpenses(accessToken, forceRefresh).collect { result -> _expenseResource.value = result }
if (response.isSuccessful) {
_expenses.value = response.body()?.expenses ?: emptyList()
}
} catch (e: Exception) {
_expenses.value = emptyList()
}
} }
} }
@@ -82,7 +83,7 @@ class MainViewModel(private val accountDao: AccountDao, private val tokenStorage
sessionState = SessionState.Valid sessionState = SessionState.Valid
// Fetch data // Fetch data
loadExpenses(account) // loadExpenses(account)
println("All data fetched") println("All data fetched")
return@launch return@launch