Improved UI and enhanced expense creation

Updated the UI across all screens (Login, Dashboard, Account Selection, Expense Details, and Creation) with a more modern, card-based design and improved layouts.

Expense creation now includes:
- Real-time split validation to ensure shares match the total amount.
- An "Equal Split" shortcut for quick allocation.
- Support for optional descriptions and multiple file attachments.
- Strict save validation.

The `MainActivity` now supports edge-to-edge display, and the `AccountDao` includes a new query to fetch accounts by ID. Added `material-icons-extended` dependency for enhanced iconography.
This commit is contained in:
2026-03-04 16:38:40 +01:00
parent 69e0344261
commit 463e1a013f
12 changed files with 793 additions and 284 deletions

View File

@@ -61,6 +61,7 @@ dependencies {
//Manually added //Manually added
implementation("androidx.datastore:datastore-preferences:1.2.0") implementation("androidx.datastore:datastore-preferences:1.2.0")
implementation("androidx.compose.material:material-icons-extended:1.6.0")
val room_version = "2.8.4" val room_version = "2.8.4"
implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-runtime:$room_version")

View File

@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState 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
@@ -21,6 +22,7 @@ import de.miaurizius.shap_planner.viewmodels.MainViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val prefs = UserPreferences(this) val prefs = UserPreferences(this)

View File

@@ -25,6 +25,9 @@ interface AccountDao {
@Query("SELECT * FROM accounts") @Query("SELECT * FROM accounts")
fun getAllAccounts(): Flow<List<Account>> fun getAllAccounts(): Flow<List<Account>>
@Query("SELECT * FROM accounts WHERE id = :userId")
fun getAccountById(userId: UUID): Flow<Account?>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAccount(account: Account) suspend fun insertAccount(account: Account)

View File

@@ -1,6 +1,5 @@
package de.miaurizius.shap_planner.repository 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.ExpenseShare
import de.miaurizius.shap_planner.entities.ExpenseShareDao import de.miaurizius.shap_planner.entities.ExpenseShareDao
import de.miaurizius.shap_planner.network.APIService import de.miaurizius.shap_planner.network.APIService

View File

@@ -1,7 +1,5 @@
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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -55,7 +53,7 @@ fun AppContent(
account = selectedAccount!!, account = selectedAccount!!,
viewModel = creationViewModel, viewModel = creationViewModel,
onBack = { showAddExpenseScreen = false }, onBack = { showAddExpenseScreen = false },
onSaved = { showAddExpenseScreen = false } onSaved = { showAddExpenseScreen = false },
) )
} }
selectedExpense != null -> { selectedExpense != null -> {

View File

@@ -1,10 +1,10 @@
package de.miaurizius.shap_planner.ui.screens package de.miaurizius.shap_planner.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -13,59 +13,176 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.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
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccountSelectionScreen(accounts: List<Account>, onAccountClick: (Account) -> Unit, onAddAccountClick: () -> Unit) { fun AccountSelectionScreen(
LazyColumn( accounts: List<Account>,
modifier = Modifier onAccountClick: (Account) -> Unit,
.fillMaxSize() onAddAccountClick: () -> Unit
.padding(16.dp) ) {
.statusBarsPadding() Scaffold(
.navigationBarsPadding(), topBar = {
verticalArrangement = Arrangement.spacedBy(12.dp) CenterAlignedTopAppBar(
) { title = { Text("ShAp-Planner", fontWeight = FontWeight.Black) }
item { )
Text("Choose an account", style = MaterialTheme.typography.headlineSmall) },
} bottomBar = {
Surface(
items(accounts) { account -> modifier = Modifier.fillMaxWidth(),
Card(modifier = Modifier.fillMaxWidth().clickable{ onAccountClick(account) }) { tonalElevation = 2.dp
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { ) {
Box(modifier = Modifier.size(40.dp).background(Color.Gray, shape = CircleShape)) Button(
Spacer(modifier = Modifier.width(16.dp)) onClick = onAddAccountClick,
Column { modifier = Modifier
Text(text = account.name, fontWeight = FontWeight.Bold) .fillMaxWidth()
Text(text = account.wgName, style = MaterialTheme.typography.bodyMedium) .padding(20.dp)
} .navigationBarsPadding()
.height(56.dp),
shape = MaterialTheme.shapes.medium
) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Add New Account", style = MaterialTheme.typography.titleMedium)
} }
} }
} }
item { ) { padding ->
Spacer(modifier = Modifier.height(8.dp)) Column(
Button( modifier = Modifier
onClick = onAddAccountClick, .fillMaxSize()
modifier = Modifier.fillMaxWidth() .padding(padding)
.padding(horizontal = 20.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Welcome back!",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "Select an account to continue",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(bottom = 16.dp)
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(bottom = 16.dp)
) { ) {
Text("Add account") if (accounts.isEmpty()) {
item {
Box(
modifier = Modifier.fillParentMaxHeight(0.6f).fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
"No accounts yet.\nTap 'Add New Account' to start.",
textAlign = TextAlign.Center,
color = Color.Gray
)
}
}
}
items(accounts) { account ->
AccountItem(account = account, onClick = { onAccountClick(account) })
}
} }
} }
} }
}
@Composable
fun AccountItem(account: Account, onClick: () -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = MaterialTheme.shapes.large,
colors = CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Stylized Avatar with Initial
Surface(
modifier = Modifier.size(52.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = account.name.take(1).uppercase(),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = account.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = account.wgName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary
)
}
}
// Trailing Chevron
Icon(
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.outline
)
}
}
} }

View File

@@ -1,7 +1,6 @@
package de.miaurizius.shap_planner.ui.screens package de.miaurizius.shap_planner.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
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
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -13,25 +12,30 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn 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.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold 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.material3.TopAppBar
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.collectAsState
import androidx.compose.runtime.getValue 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.text.font.FontWeight 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
@@ -43,115 +47,116 @@ import de.miaurizius.shap_planner.viewmodels.MainViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
// Data and regarding Methods
account: Account, account: Account,
onExpenseClick: (Expense) -> Unit, onExpenseClick: (Expense) -> Unit,
onAddExpenseClick: () -> Unit, onAddExpenseClick: () -> Unit,
// Default Methods
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
onBack: () -> Unit, onBack: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
sessionState: SessionState, sessionState: SessionState,
onValidate: () -> Unit, onValidate: () -> Unit,
onSessionInvalid: () -> Unit) { onSessionInvalid: () -> Unit
) {
val expenseResource by mainViewModel.expenseResource.collectAsState() val expenseResource by mainViewModel.expenseResource.collectAsState()
LaunchedEffect(Unit) { onValidate() } LaunchedEffect(Unit) { onValidate() }
LaunchedEffect(account) { mainViewModel.loadExpenses(account, forceRefresh = false) } LaunchedEffect(account) { mainViewModel.loadExpenses(account, forceRefresh = false) }
when (sessionState) {
SessionState.Loading -> { if (sessionState == SessionState.Invalid) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LaunchedEffect(Unit) { onSessionInvalid() }
CircularProgressIndicator() return
}
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text(account.wgName, style = MaterialTheme.typography.titleLarge)
Text("Household", style = MaterialTheme.typography.labelSmall)
}
},
actions = {
IconButton(onClick = onBack) {
Icon(Icons.Default.Refresh, "Switch")
}
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, "Delete", tint = MaterialTheme.colorScheme.error)
}
}
)
},
floatingActionButton = {
androidx.compose.material3.FloatingActionButton(
onClick = onAddExpenseClick,
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) {
Icon(Icons.Default.Add, "Neu")
} }
} }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(MaterialTheme.colorScheme.surface)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, ${account.name}!",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
SessionState.Valid -> { Spacer(modifier = Modifier.height(16.dp))
BackHandler {
onBack() SummaryCard(expenses = expenseResource.data ?: emptyList())
} }
Scaffold(floatingActionButton = {
androidx.compose.material3.FloatingActionButton( Text(
onClick = onAddExpenseClick, text = "Latest expenses",
containerColor = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium,
contentColor = MaterialTheme.colorScheme.onPrimary modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
) { color = MaterialTheme.colorScheme.secondary
Text("+", style = MaterialTheme.typography.headlineSmall) )
Box(modifier = Modifier.fillMaxSize()) {
if (expenseResource is Resource.Loading && expenseResource.data?.isEmpty() == true) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} }
}) { paddingValues ->
Column( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize() contentPadding = PaddingValues(16.dp),
.padding(paddingValues) verticalArrangement = Arrangement.spacedBy(12.dp)
.padding(16.dp)
.statusBarsPadding()
.navigationBarsPadding()
) { ) {
// Header items(expenseResource.data ?: emptyList()) { expense ->
Row( ExpenseItem(expense = expense, onClick = { onExpenseClick(expense) })
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "Hello, ${account.name}!",
style = MaterialTheme.typography.headlineMedium
)
Text(
text = "Household: ${account.wgName}",
style = MaterialTheme.typography.bodyLarge,
color = Color.Gray
)
}
Button(onClick = onBack) {
Text("Switch")
}
}
Spacer(modifier = Modifier.height(5.dp))
Button(onClick = onDelete) {
Text("Delete")
}
Spacer(modifier = Modifier.height(10.dp))
Text("Costs", style = MaterialTheme.typography.titleLarge)
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(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant, shape = MaterialTheme.shapes.medium),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(expenseResource.data ?: emptyList()) { expense ->
ExpenseItem(expense = expense, onClick = { onExpenseClick(expense) })
}
} }
} }
} }
} }
}
}
SessionState.Invalid -> { @SuppressLint("DefaultLocale")
LaunchedEffect(Unit) { @Composable
onSessionInvalid() fun SummaryCard(expenses: List<Expense>) {
} val total = expenses.sumOf { it.amount } / 100.0
} androidx.compose.material3.Card(
modifier = Modifier.fillMaxWidth(),
is SessionState.Error -> { colors = androidx.compose.material3.CardDefaults.cardColors(
Text("Server error") containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Column(modifier = Modifier.padding(20.dp)) {
Text("Total expenditure", style = MaterialTheme.typography.labelMedium)
Text(
text = String.format("%.2f €", total),
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.Black
)
} }
} }
} }
@@ -159,14 +164,54 @@ fun DashboardScreen(
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
@Composable @Composable
fun ExpenseItem(expense: Expense, onClick: () -> Unit) { fun ExpenseItem(expense: Expense, onClick: () -> Unit) {
Surface(modifier = Modifier androidx.compose.material3.ElevatedCard(
.fillMaxWidth() modifier = Modifier
.clickable{onClick()}, .fillMaxWidth()
shape = MaterialTheme.shapes.small, .clickable { onClick() },
color = MaterialTheme.colorScheme.surface) { shape = MaterialTheme.shapes.medium,
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween) { colors = androidx.compose.material3.CardDefaults.elevatedCardColors(
Text(text = expense.title, style = MaterialTheme.typography.bodyLarge) containerColor = MaterialTheme.colorScheme.surface
Text(text = String.format("%.2f€", expense.amount / 100.0), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold) )
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Surface(
shape = androidx.compose.foundation.shape.CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(40.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = expense.title.take(1).uppercase(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = expense.title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
Text(
text = String.format("%.2f €", expense.amount / 100.0),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.ExtraBold,
color = MaterialTheme.colorScheme.primary
)
} }
} }
} }

View File

@@ -1,17 +1,28 @@
package de.miaurizius.shap_planner.ui.screens package de.miaurizius.shap_planner.ui.screens
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Description
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.viewmodels.ExpenseCreationViewModel import de.miaurizius.shap_planner.viewmodels.ExpenseCreationViewModel
import java.util.UUID
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @SuppressLint("DefaultLocale")
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ExpenseCreationScreen( fun ExpenseCreationScreen(
account: Account, account: Account,
@@ -20,68 +31,219 @@ fun ExpenseCreationScreen(
onBack: () -> Unit onBack: () -> Unit
) { ) {
var title by remember { mutableStateOf("") } var title by remember { mutableStateOf("") }
var amountStr by remember { mutableStateOf("") } var description by remember { mutableStateOf("") }
var totalAmountStr by remember { mutableStateOf("") }
val attachmentUris = remember { mutableStateListOf<android.net.Uri>() }
val userShares = remember { mutableStateMapOf<java.util.UUID, String>() }
val users by viewModel.users.collectAsState() val users by viewModel.users.collectAsState()
val selectedUsers = remember { mutableStateListOf<UUID>() }
// Real-time calculation logic
val totalCents = (totalAmountStr.replace(",", ".").toDoubleOrNull() ?: 0.0) * 100
val distributedCents = userShares.values.sumOf {
(it.replace(",", ".").toDoubleOrNull() ?: 0.0) * 100
}.toLong()
val diff = totalCents.toLong() - distributedCents
val isAmountMatched = totalCents > 0 && diff == 0L
// File Picker for multiple files (Images & PDFs)
val launcher = androidx.activity.compose.rememberLauncherForActivityResult(
contract = androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents()
) { uris ->
attachmentUris.addAll(uris)
}
LaunchedEffect(Unit) { viewModel.loadUsers() } LaunchedEffect(Unit) { viewModel.loadUsers() }
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar(title = { Text("New Expense") }) TopAppBar(
title = { Text("New Expense") },
navigationIcon = { IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
} }
)
} }
) { padding -> ) { padding ->
Column(modifier = Modifier.padding(padding).padding(16.dp)) { LazyColumn(
OutlinedTextField( modifier = Modifier.fillMaxSize().padding(padding).padding(horizontal = 16.dp),
value = title, verticalArrangement = Arrangement.spacedBy(16.dp)
onValueChange = { title = it }, ) {
label = { Text("What was bought?") }, // Header Info
modifier = Modifier.fillMaxWidth() item {
) Spacer(modifier = Modifier.height(8.dp))
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(8.dp)) Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
OutlinedTextField( value = title,
value = amountStr, onValueChange = { title = it },
onValueChange = { amountStr = it }, label = { Text("Title *") },
label = { Text("Amount in €") }, modifier = Modifier.fillMaxWidth()
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), )
modifier = Modifier.fillMaxWidth() Spacer(modifier = Modifier.height(12.dp))
) OutlinedTextField(
value = totalAmountStr,
Spacer(modifier = Modifier.height(16.dp)) onValueChange = { totalAmountStr = it },
Text("Who participated?", style = MaterialTheme.typography.titleMedium) label = { Text("Total Amount (€) *") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
FlowRow(modifier = Modifier.padding(vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { modifier = Modifier.fillMaxWidth()
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)) // Optional Description & Multi-File Attachments
item {
Text("Additional Info", style = MaterialTheme.typography.titleMedium)
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Description (Optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 2
)
Spacer(modifier = Modifier.height(16.dp))
Button( Text("Attachments (${attachmentUris.size})", style = MaterialTheme.typography.labelLarge)
onClick = { attachmentUris.forEach { uri ->
val cents = (amountStr.replace(",", ".").toDoubleOrNull() ?: 0.0) * 100 Row(verticalAlignment = Alignment.CenterVertically) {
viewModel.saveExpense(account, title, cents.toInt(), selectedUsers.toList()) Icon(Icons.Default.Description, "File", modifier = Modifier.size(20.dp))
onSaved() Text(" Document attached", modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodySmall)
}, IconButton(onClick = { attachmentUris.remove(uri) }) {
modifier = Modifier.fillMaxWidth(), Icon(Icons.Default.Delete, "Remove", tint = Color.Red)
enabled = title.isNotBlank() && amountStr.isNotBlank() && selectedUsers.isNotEmpty() }
) { }
Text("Save") }
Button(onClick = { launcher.launch("*/*") }) {
Text("Select Files (Images, PDF)")
}
}
}
} }
TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) { // Split Details & Validation Message
Text("Cancel") item {
Column {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("Split Details *", style = MaterialTheme.typography.titleMedium)
TextButton(onClick = {
if (totalCents > 0) {
val share = String.format("%.2f", (totalCents / users.size) / 100.0)
users.forEach { userShares[it.id] = share }
}
}) { Text("Split Equally") }
}
// VALIDATION MESSAGE
if (totalCents > 0) {
val statusText = when {
diff > 0 -> "Remaining: ${String.format("%.2f", diff / 100.0)}"
diff < 0 -> "Over-allocated: ${String.format("%.2f", Math.abs(diff) / 100.0)}"
else -> "Amount matched! ✓"
}
val statusColor = if (diff == 0L) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
Surface(
color = statusColor.copy(alpha = 0.1f),
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
) {
Text(
text = statusText,
color = statusColor,
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}
}
items(users) { user ->
UserShareInputItem(
userName = user.name,
amount = userShares[user.id] ?: "",
onAmountChange = { userShares[user.id] = it }
)
}
// Save Actions
item {
Button(
onClick = {
viewModel.saveExpense(
account = account,
title = title,
description = description,
amountCents = totalCents.toInt(),
shares = userShares.mapValues { (it.value.replace(",",".").toDoubleOrNull() ?: 0.0).toInt() * 100 }.filter { it.value > 0 },
attachments = attachmentUris.map { it.toString() }
)
onSaved()
},
modifier = Modifier.fillMaxWidth().height(56.dp),
enabled = title.isNotBlank() && isAmountMatched // STRICT VALIDATION
) {
Text("Save Expense")
}
if (totalAmountStr.isNotBlank() && !isAmountMatched) {
Text(
"Sum of shares must equal total amount to save.",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
textAlign = TextAlign.Center
)
}
TextButton(onClick = onBack, modifier = Modifier.fillMaxWidth()) {
Text("Cancel")
}
Spacer(modifier = Modifier.height(32.dp))
} }
} }
} }
}
@Composable
fun UserShareInputItem(userName: String, amount: String, onAmountChange: (String) -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = androidx.compose.foundation.shape.CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(32.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(userName.take(1).uppercase(), style = MaterialTheme.typography.bodySmall)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(userName, modifier = Modifier.weight(1f), style = MaterialTheme.typography.bodyLarge)
OutlinedTextField(
value = amount,
onValueChange = onAmountChange,
modifier = Modifier.width(100.dp),
placeholder = { Text("0.00") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
textStyle = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End)
)
Spacer(modifier = Modifier.width(4.dp))
Text("")
}
}
} }

View File

@@ -2,24 +2,26 @@ package de.miaurizius.shap_planner.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -54,32 +56,81 @@ fun ExpenseDetailScreen(
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(expense.title) }, title = { Text("Expense Details") },
navigationIcon = { IconButton(onClick = onBack) { navigationIcon = {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") IconButton(onClick = onBack) {
} } Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
) )
} }
) { padding -> ) { padding ->
Column(modifier = Modifier.padding(padding).padding(16.dp)) { Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
// Hero Section: Amount & Title
Column(
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Total Amount",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = String.format("%.2f €", expense.amount / 100.0),
style = MaterialTheme.typography.displayMedium,
fontWeight = FontWeight.Black,
color = MaterialTheme.colorScheme.primary
)
Text(
text = expense.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
// Description Card (only shown if not empty)
if (expense.description.isNotBlank()) {
androidx.compose.material3.ElevatedCard(
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
colors = androidx.compose.material3.CardDefaults.elevatedCardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Description",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
Text(text = expense.description, style = MaterialTheme.typography.bodyLarge)
}
}
}
Text( Text(
text = String.format("%.2f€", expense.amount / 100.0), text = "Cost Distribution",
style = MaterialTheme.typography.displayMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary modifier = Modifier.padding(bottom = 12.dp)
) )
Text(expense.description, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(24.dp)) LazyColumn(
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) verticalArrangement = Arrangement.spacedBy(12.dp),
Spacer(modifier = Modifier.height(16.dp)) modifier = Modifier.fillMaxSize()
) {
Text("Cost Allocation", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(shares) { item -> items(shares) { item ->
ShareItem(item.user?.name ?: "Unknown User", item.share.share_cents) ShareItem(
name = item.user?.name ?: "Unknown User",
amountCents = item.share.share_cents
)
} }
} }
} }
@@ -89,20 +140,49 @@ fun ExpenseDetailScreen(
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
@Composable @Composable
fun ShareItem(name: String, amountCents: Int) { fun ShareItem(name: String, amountCents: Int) {
Card(modifier = Modifier androidx.compose.material3.OutlinedCard(
.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
.padding(vertical = 4.dp) shape = MaterialTheme.shapes.medium,
colors = androidx.compose.material3.CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text(name, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f)) // User Avatar Circle
Surface(
shape = androidx.compose.foundation.shape.CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(36.dp)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = name.take(1).uppercase(),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text( Text(
text = String.format("%.2f€", amountCents / 100.0), text = name,
color = if (amountCents > 0) Color(0xFF4CAF50) else Color.Gray, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
modifier = Modifier.weight(1f)
)
Text(
text = String.format("%.2f €", amountCents / 100.0),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyLarge color = if (amountCents > 0) Color(0xFF2E7D32) else Color.Gray
) )
} }
} }

View File

@@ -3,70 +3,174 @@ package de.miaurizius.shap_planner.ui.screens
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
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.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.VpnKey
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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 androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class) @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
) {
if (onBack != null) { if (onBack != null) {
BackHandler { BackHandler { onBack() }
onBack()
}
} }
var serverUrl by remember { mutableStateOf("") } var serverUrl by remember { mutableStateOf("") }
var username by remember { mutableStateOf("") } var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
Column(modifier = Modifier.padding(16.dp).statusBarsPadding().navigationBarsPadding()) { Scaffold(
Text("Bitte anmelden") topBar = {
Spacer(modifier = Modifier.height(8.dp)) TopAppBar(
title = { Text("Add Account", fontWeight = FontWeight.Bold) },
navigationIcon = {
if (onBack != null) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp)
.verticalScroll(rememberScrollState()), // Scrollbar, falls die Tastatur den Platz wegnimmt
horizontalAlignment = Alignment.CenterHorizontally
) {
// App Icon or Welcome Text
Icon(
imageVector = Icons.Default.Lock,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
//Home-Server Text(
TextField( text = "Connect to Server",
value = serverUrl, style = MaterialTheme.typography.headlineMedium,
onValueChange = { serverUrl = it }, fontWeight = FontWeight.Bold,
label = { Text("Server-Domain") } modifier = Modifier.padding(top = 16.dp)
) )
Spacer(modifier = Modifier.height(8.dp))
//Username Text(
TextField( text = "Enter your credentials to link your account",
value = username, style = MaterialTheme.typography.bodyMedium,
onValueChange = { username = it }, color = MaterialTheme.colorScheme.secondary,
label = { Text("Nutzername") } textAlign = TextAlign.Center,
) modifier = Modifier.padding(bottom = 32.dp)
Spacer(modifier = Modifier.height(8.dp)) )
//Password // Server URL Field
TextField( OutlinedTextField(
value = password, value = serverUrl,
onValueChange = { password = it }, onValueChange = { serverUrl = it },
label = { Text("Passwort") } label = { Text("Server URL") },
) placeholder = { Text("your-server.com") },
Spacer(modifier = Modifier.height(8.dp)) modifier = Modifier.fillMaxWidth(),
singleLine = true,
leadingIcon = { Icon(Icons.Default.Cloud, contentDescription = null) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next)
)
Button(onClick = { if(serverUrl.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()) onLogin( Spacer(modifier = Modifier.height(16.dp))
serverUrl,
username, // Username Field
password OutlinedTextField(
) }) { value = username,
Text("Login") onValueChange = { username = it },
label = { Text("Username") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)
Spacer(modifier = Modifier.height(16.dp))
// Password Field
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
leadingIcon = { Icon(Icons.Default.VpnKey, contentDescription = null) },
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
trailingIcon = {
val image = if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, contentDescription = "Toggle password visibility")
}
}
)
Spacer(modifier = Modifier.height(32.dp))
// Login Button
Button(
onClick = { onLogin(serverUrl, username, password) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = serverUrl.isNotBlank() && username.isNotBlank() && password.isNotBlank(),
shape = MaterialTheme.shapes.medium
) {
Text("Connect Account", style = MaterialTheme.typography.titleMedium)
}
if (onBack != null) {
TextButton(onClick = onBack, modifier = Modifier.padding(top = 8.dp)) {
Text("Cancel", color = MaterialTheme.colorScheme.secondary)
}
}
} }
} }
} }

View File

@@ -30,35 +30,37 @@ class ExpenseCreationViewModel(
} }
} }
fun saveExpense(account: Account, title: String, amountCents: Int, selectedUserIds: List<UUID>) { fun saveExpense(
viewModelScope.launch { account: Account,
val expenseId = UUID.randomUUID() //TODO: Backend has to generate UUID title: String,
val newExpense = Expense( description: String,
id = expenseId, amountCents: Int,
payer_id = account.id, shares: Map<UUID, Int>,
amount = amountCents, attachments: List<String>
title = title, ) {
description = "", // viewModelScope.launch {
attachments = null, // val expenseId = UUID.randomUUID()
created_at = (System.currentTimeMillis() / 1000).toInt(), // val newExpense = Expense(
last_updated_at = 0 // id = expenseId,
) // payer_id = account.id,
// amount = amountCents,
expenseDao.insertExpense(newExpense) // title = title,
// description = description,
val shareAmount = amountCents / selectedUserIds.size // attachments = if (attachments.isEmpty()) null else attachments,
selectedUserIds.forEach { userId -> // created_at = (System.currentTimeMillis() / 1000).toInt(),
shareDao.insertShare( // last_updated_at = 0
ExpenseShare( // )
UUID.randomUUID(), //TODO: Backend has to generate UUID //
expenseId, // expenseDao.insertExpense(newExpense)
userId, //
shareAmount // shares.forEach { (userId, shareCents) ->
) // shareDao.insertShare(
) // ExpenseShare(UUID.randomUUID(), expenseId, userId, shareCents)
} // )
// }
// API Calls //
} // // API POST Request
//
// }
} }
} }

View File

@@ -82,10 +82,6 @@ class MainViewModel(
sessionState = SessionState.Valid sessionState = SessionState.Valid
// Fetch data
// loadExpenses(account)
println("All data fetched")
return@launch return@launch
} else { } else {
sessionState = SessionState.Invalid sessionState = SessionState.Invalid