Too much to split into individual commits:
- updated theme - changed title to budget name on all main tab pages - various fixes to state handling Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
c23c16ab40
commit
ed4fd514f1
22 changed files with 717 additions and 166 deletions
|
@ -1,6 +1,7 @@
|
||||||
package com.wbrawner.budget.ui
|
package com.wbrawner.budget.ui
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
@ -35,6 +36,7 @@ import com.wbrawner.budget.ui.recurringtransaction.RecurringTransactionDetailsSc
|
||||||
import com.wbrawner.budget.ui.recurringtransaction.RecurringTransactionsScreen
|
import com.wbrawner.budget.ui.recurringtransaction.RecurringTransactionsScreen
|
||||||
import com.wbrawner.budget.ui.transaction.TransactionDetailsScreen
|
import com.wbrawner.budget.ui.transaction.TransactionDetailsScreen
|
||||||
import com.wbrawner.budget.ui.transaction.TransactionsScreen
|
import com.wbrawner.budget.ui.transaction.TransactionsScreen
|
||||||
|
import com.wbrawner.twigs.shared.Action
|
||||||
import com.wbrawner.twigs.shared.Route
|
import com.wbrawner.twigs.shared.Route
|
||||||
import com.wbrawner.twigs.shared.Store
|
import com.wbrawner.twigs.shared.Store
|
||||||
import com.wbrawner.twigs.shared.budget.BudgetAction
|
import com.wbrawner.twigs.shared.budget.BudgetAction
|
||||||
|
@ -63,6 +65,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
TwigsApp {
|
TwigsApp {
|
||||||
val authViewModel: AuthViewModel = hiltViewModel()
|
val authViewModel: AuthViewModel = hiltViewModel()
|
||||||
|
BackHandler {
|
||||||
|
store.dispatch(Action.Back)
|
||||||
|
}
|
||||||
NavHost(navController, state.initialRoute.path) {
|
NavHost(navController, state.initialRoute.path) {
|
||||||
composable(Route.Login.path) {
|
composable(Route.Login.path) {
|
||||||
LoginScreen(store = store, viewModel = authViewModel)
|
LoginScreen(store = store, viewModel = authViewModel)
|
||||||
|
|
|
@ -1,16 +1,35 @@
|
||||||
package com.wbrawner.budget.ui
|
package com.wbrawner.budget.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.scrollable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
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
|
||||||
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.wbrawner.budget.ui.base.TwigsColors
|
||||||
|
import com.wbrawner.budget.ui.base.TwigsTheme
|
||||||
import com.wbrawner.budget.ui.transaction.toCurrencyString
|
import com.wbrawner.budget.ui.transaction.toCurrencyString
|
||||||
import com.wbrawner.twigs.shared.Store
|
import com.wbrawner.twigs.shared.Store
|
||||||
|
|
||||||
|
@ -18,22 +37,173 @@ import com.wbrawner.twigs.shared.Store
|
||||||
@Composable
|
@Composable
|
||||||
fun OverviewScreen(store: Store) {
|
fun OverviewScreen(store: Store) {
|
||||||
val state by store.state.collectAsState()
|
val state by store.state.collectAsState()
|
||||||
TwigsScaffold(store = store, title = "Overview") { padding ->
|
val budget = state.selectedBudget?.let { id -> state.budgets?.first { it.id == id } }
|
||||||
val budget = state.selectedBudget?.let { id -> state.budgets?.first { it.id == id } }
|
TwigsScaffold(store = store, title = budget?.name ?: "Select a Budget") { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.scrollable(rememberScrollState(), orientation = Orientation.Vertical)
|
||||||
verticalArrangement = Arrangement.Center
|
.padding(8.dp),
|
||||||
|
verticalArrangement = spacedBy(8.dp, alignment = Alignment.Top)
|
||||||
) {
|
) {
|
||||||
budget?.let { budget ->
|
budget?.description?.let { description ->
|
||||||
Text(budget.name)
|
if (description.isNotBlank()) {
|
||||||
Text(budget.description ?: "")
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(description, style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text("Cash Flow")
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(state.budgetBalance?.toCurrencyString() ?: "-")
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
LabeledField(
|
||||||
|
label = "Cash Flow",
|
||||||
|
value = state.budgetBalance?.toCurrencyString() ?: "-"
|
||||||
|
)
|
||||||
|
LabeledField(
|
||||||
|
label = "Transactions",
|
||||||
|
value = state.transactions?.size?.toString() ?: "-"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CashFlowChart(
|
||||||
|
expectedIncome = state.expectedIncome,
|
||||||
|
actualIncome = state.actualIncome,
|
||||||
|
expectedExpenses = state.expectedExpenses,
|
||||||
|
actualExpenses = state.actualExpenses
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CashFlowChart(
|
||||||
|
expectedIncome: Long?,
|
||||||
|
actualIncome: Long?,
|
||||||
|
expectedExpenses: Long?,
|
||||||
|
actualExpenses: Long?,
|
||||||
|
) {
|
||||||
|
val maxValue = listOfNotNull(expectedIncome, expectedExpenses, actualIncome, actualExpenses)
|
||||||
|
.maxOrNull()
|
||||||
|
?.toFloat()
|
||||||
|
?: 0f
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
CashFlowProgressBar(
|
||||||
|
label = "Expected Income",
|
||||||
|
value = expectedIncome,
|
||||||
|
maxValue = maxValue,
|
||||||
|
color = TwigsColors.DarkGreen,
|
||||||
|
trackColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
CashFlowProgressBar(
|
||||||
|
label = "Actual Income",
|
||||||
|
value = actualIncome,
|
||||||
|
maxValue = maxValue,
|
||||||
|
color = TwigsColors.Green,
|
||||||
|
trackColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
CashFlowProgressBar(
|
||||||
|
label = "Expected Expenses",
|
||||||
|
value = expectedExpenses,
|
||||||
|
maxValue = maxValue,
|
||||||
|
color = TwigsColors.DarkRed,
|
||||||
|
trackColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
CashFlowProgressBar(
|
||||||
|
label = "Actual Expenses",
|
||||||
|
value = actualExpenses,
|
||||||
|
maxValue = maxValue,
|
||||||
|
color = TwigsColors.Red,
|
||||||
|
trackColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CashFlowProgressBar(
|
||||||
|
label: String,
|
||||||
|
value: Long?,
|
||||||
|
maxValue: Float,
|
||||||
|
color: Color,
|
||||||
|
trackColor: Color
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = spacedBy(4.dp)) {
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Text(label)
|
||||||
|
Text(value?.toCurrencyString() ?: "-")
|
||||||
|
}
|
||||||
|
value?.let {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp)),
|
||||||
|
progress = it.toFloat() / maxValue,
|
||||||
|
color = color,
|
||||||
|
trackColor = trackColor,
|
||||||
|
)
|
||||||
|
} ?: LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
color = color,
|
||||||
|
trackColor = trackColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabeledField(label: String, value: String) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(text = label, style = MaterialTheme.typography.labelMedium)
|
||||||
|
Text(text = value, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun CashFlowChart_Preview() {
|
||||||
|
TwigsTheme {
|
||||||
|
CashFlowChart(
|
||||||
|
expectedIncome = 100,
|
||||||
|
actualIncome = 50,
|
||||||
|
expectedExpenses = 80,
|
||||||
|
actualExpenses = 95
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LabeledField_Preview() {
|
||||||
|
TwigsTheme {
|
||||||
|
LabeledField(
|
||||||
|
label = "Transactions",
|
||||||
|
value = "250"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import androidx.compose.foundation.verticalScroll
|
||||||
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
@ -34,7 +35,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.isShiftPressed
|
import androidx.compose.ui.input.key.isShiftPressed
|
||||||
|
@ -140,7 +140,7 @@ fun LoginForm(
|
||||||
)
|
)
|
||||||
Text("Log in to manage your budgets")
|
Text("Log in to manage your budgets")
|
||||||
if (error.isNotBlank()) {
|
if (error.isNotBlank()) {
|
||||||
Text(text = error, color = Color.Red)
|
Text(text = error, color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package com.wbrawner.budget.ui.base
|
package com.wbrawner.budget.ui.base
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Green300 = Color(0xFF81C784)
|
val Green300 = Color(0xFF81C784)
|
||||||
|
@ -11,3 +13,82 @@ val Red500 = Color(0xFFF44336)
|
||||||
val Red700 = Color(0xFFD32F2F)
|
val Red700 = Color(0xFFD32F2F)
|
||||||
val Red900 = Color(0xFFB71C1C)
|
val Red900 = Color(0xFFB71C1C)
|
||||||
|
|
||||||
|
object TwigsColors {
|
||||||
|
val Green
|
||||||
|
@Composable
|
||||||
|
get() = if (isSystemInDarkTheme()) Green300 else Green700
|
||||||
|
val DarkGreen
|
||||||
|
@Composable
|
||||||
|
get() = if (isSystemInDarkTheme()) Green500 else Green900
|
||||||
|
val Red
|
||||||
|
@Composable
|
||||||
|
get() = if (isSystemInDarkTheme()) Red300 else Red700
|
||||||
|
val DarkRed
|
||||||
|
@Composable
|
||||||
|
get() = if (isSystemInDarkTheme()) Red500 else Red900
|
||||||
|
}
|
||||||
|
|
||||||
|
val md_theme_light_primary = Color(0xFF006E26)
|
||||||
|
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_primaryContainer = Color(0xFF6CFF82)
|
||||||
|
val md_theme_light_onPrimaryContainer = Color(0xFF002106)
|
||||||
|
val md_theme_light_secondary = Color(0xFF526350)
|
||||||
|
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_secondaryContainer = Color(0xFFD5E8D0)
|
||||||
|
val md_theme_light_onSecondaryContainer = Color(0xFF101F10)
|
||||||
|
val md_theme_light_tertiary = Color(0xFF39656B)
|
||||||
|
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_tertiaryContainer = Color(0xFFBCEBF2)
|
||||||
|
val md_theme_light_onTertiaryContainer = Color(0xFF001F23)
|
||||||
|
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||||
|
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||||
|
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||||
|
val md_theme_light_background = Color(0xFFFCFDF7)
|
||||||
|
val md_theme_light_onBackground = Color(0xFF1A1C19)
|
||||||
|
val md_theme_light_surface = Color(0xFFFCFDF7)
|
||||||
|
val md_theme_light_onSurface = Color(0xFF1A1C19)
|
||||||
|
val md_theme_light_surfaceVariant = Color(0xFFDEE5D9)
|
||||||
|
val md_theme_light_onSurfaceVariant = Color(0xFF424940)
|
||||||
|
val md_theme_light_outline = Color(0xFF72796F)
|
||||||
|
val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB)
|
||||||
|
val md_theme_light_inverseSurface = Color(0xFF2F312D)
|
||||||
|
val md_theme_light_inversePrimary = Color(0xFF47E266)
|
||||||
|
val md_theme_light_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_light_surfaceTint = Color(0xFF006E26)
|
||||||
|
val md_theme_light_outlineVariant = Color(0xFFC2C9BD)
|
||||||
|
val md_theme_light_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
val md_theme_dark_primary = Color(0xFF47E266)
|
||||||
|
val md_theme_dark_onPrimary = Color(0xFF003910)
|
||||||
|
val md_theme_dark_primaryContainer = Color(0xFF00531A)
|
||||||
|
val md_theme_dark_onPrimaryContainer = Color(0xFF6CFF82)
|
||||||
|
val md_theme_dark_secondary = Color(0xFFB9CCB4)
|
||||||
|
val md_theme_dark_onSecondary = Color(0xFF243424)
|
||||||
|
val md_theme_dark_secondaryContainer = Color(0xFF3A4B39)
|
||||||
|
val md_theme_dark_onSecondaryContainer = Color(0xFFD5E8D0)
|
||||||
|
val md_theme_dark_tertiary = Color(0xFFA1CED5)
|
||||||
|
val md_theme_dark_onTertiary = Color(0xFF00363C)
|
||||||
|
val md_theme_dark_tertiaryContainer = Color(0xFF1F4D53)
|
||||||
|
val md_theme_dark_onTertiaryContainer = Color(0xFFBCEBF2)
|
||||||
|
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||||
|
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||||
|
val md_theme_dark_onError = Color(0xFF690005)
|
||||||
|
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
val md_theme_dark_background = Color(0xFF1A1C19)
|
||||||
|
val md_theme_dark_onBackground = Color(0xFFE2E3DD)
|
||||||
|
val md_theme_dark_surface = Color(0xFF1A1C19)
|
||||||
|
val md_theme_dark_onSurface = Color(0xFFE2E3DD)
|
||||||
|
val md_theme_dark_surfaceVariant = Color(0xFF424940)
|
||||||
|
val md_theme_dark_onSurfaceVariant = Color(0xFFC2C9BD)
|
||||||
|
val md_theme_dark_outline = Color(0xFF8C9388)
|
||||||
|
val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19)
|
||||||
|
val md_theme_dark_inverseSurface = Color(0xFFE2E3DD)
|
||||||
|
val md_theme_dark_inversePrimary = Color(0xFF006E26)
|
||||||
|
val md_theme_dark_shadow = Color(0xFF000000)
|
||||||
|
val md_theme_dark_surfaceTint = Color(0xFF47E266)
|
||||||
|
val md_theme_dark_outlineVariant = Color(0xFF424940)
|
||||||
|
val md_theme_dark_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
|
|
||||||
|
val seed = Color(0xFF30D158)
|
||||||
|
|
|
@ -4,36 +4,77 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import com.wbrawner.budget.R
|
import com.wbrawner.budget.R
|
||||||
|
|
||||||
val lightColors = lightColorScheme(
|
private val LightColors = lightColorScheme(
|
||||||
primary = Green500,
|
primary = md_theme_light_primary,
|
||||||
primaryContainer = Green300,
|
onPrimary = md_theme_light_onPrimary,
|
||||||
secondary = Green700,
|
primaryContainer = md_theme_light_primaryContainer,
|
||||||
secondaryContainer = Green300,
|
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||||
background = Color.LightGray,
|
secondary = md_theme_light_secondary,
|
||||||
surface = Color.White,
|
onSecondary = md_theme_light_onSecondary,
|
||||||
surfaceVariant = Color.White
|
secondaryContainer = md_theme_light_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_light_tertiary,
|
||||||
|
onTertiary = md_theme_light_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
|
error = md_theme_light_error,
|
||||||
|
errorContainer = md_theme_light_errorContainer,
|
||||||
|
onError = md_theme_light_onError,
|
||||||
|
onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
|
background = md_theme_light_background,
|
||||||
|
onBackground = md_theme_light_onBackground,
|
||||||
|
surface = md_theme_light_surface,
|
||||||
|
onSurface = md_theme_light_onSurface,
|
||||||
|
surfaceVariant = md_theme_light_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||||
|
outline = md_theme_light_outline,
|
||||||
|
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_light_inverseSurface,
|
||||||
|
inversePrimary = md_theme_light_inversePrimary,
|
||||||
|
surfaceTint = md_theme_light_surfaceTint,
|
||||||
|
outlineVariant = md_theme_light_outlineVariant,
|
||||||
|
scrim = md_theme_light_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
val darkColors = darkColorScheme(
|
|
||||||
primary = Green300,
|
private val DarkColors = darkColorScheme(
|
||||||
primaryContainer = Green500,
|
primary = md_theme_dark_primary,
|
||||||
secondary = Green500,
|
onPrimary = md_theme_dark_onPrimary,
|
||||||
secondaryContainer = Green700,
|
primaryContainer = md_theme_dark_primaryContainer,
|
||||||
background = Color.Black,
|
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||||
surface = Color.Black,
|
secondary = md_theme_dark_secondary,
|
||||||
surfaceVariant = Color.White.copy(alpha = 0.1f)
|
onSecondary = md_theme_dark_onSecondary,
|
||||||
|
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||||
|
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||||
|
tertiary = md_theme_dark_tertiary,
|
||||||
|
onTertiary = md_theme_dark_onTertiary,
|
||||||
|
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||||
|
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||||
|
error = md_theme_dark_error,
|
||||||
|
errorContainer = md_theme_dark_errorContainer,
|
||||||
|
onError = md_theme_dark_onError,
|
||||||
|
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||||
|
background = md_theme_dark_background,
|
||||||
|
onBackground = md_theme_dark_onBackground,
|
||||||
|
surface = md_theme_dark_surface,
|
||||||
|
onSurface = md_theme_dark_onSurface,
|
||||||
|
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||||
|
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||||
|
outline = md_theme_dark_outline,
|
||||||
|
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||||
|
inverseSurface = md_theme_dark_inverseSurface,
|
||||||
|
inversePrimary = md_theme_dark_inversePrimary,
|
||||||
|
surfaceTint = md_theme_dark_surfaceTint,
|
||||||
|
outlineVariant = md_theme_dark_outlineVariant,
|
||||||
|
scrim = md_theme_dark_scrim,
|
||||||
)
|
)
|
||||||
|
|
||||||
val ubuntu = FontFamily(
|
val ubuntu = FontFamily(
|
||||||
|
@ -48,9 +89,7 @@ val ubuntu = FontFamily(
|
||||||
@Composable
|
@Composable
|
||||||
fun TwigsTheme(darkMode: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
fun TwigsTheme(darkMode: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = if (darkMode) dynamicDarkColorScheme(LocalContext.current) else dynamicLightColorScheme(
|
colorScheme = if (darkMode) DarkColors else LightColors,
|
||||||
LocalContext.current
|
|
||||||
),
|
|
||||||
typography = MaterialTheme.typography.copy(
|
typography = MaterialTheme.typography.copy(
|
||||||
displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = ubuntu),
|
displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = ubuntu),
|
||||||
displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = ubuntu),
|
displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = ubuntu),
|
||||||
|
|
|
@ -2,13 +2,12 @@ package com.wbrawner.budget.ui.category
|
||||||
|
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -17,14 +16,19 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.budget.ui.TwigsScaffold
|
import com.wbrawner.budget.ui.TwigsScaffold
|
||||||
import com.wbrawner.budget.ui.base.TwigsApp
|
import com.wbrawner.budget.ui.base.TwigsApp
|
||||||
|
import com.wbrawner.budget.ui.transaction.toCurrencyString
|
||||||
import com.wbrawner.twigs.shared.Store
|
import com.wbrawner.twigs.shared.Store
|
||||||
import com.wbrawner.twigs.shared.category.Category
|
import com.wbrawner.twigs.shared.category.Category
|
||||||
import com.wbrawner.twigs.shared.category.CategoryAction
|
import com.wbrawner.twigs.shared.category.CategoryAction
|
||||||
|
import com.wbrawner.twigs.shared.category.groupByType
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.capitalizedName
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
@ -32,25 +36,37 @@ import kotlin.math.max
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoriesScreen(store: Store) {
|
fun CategoriesScreen(store: Store) {
|
||||||
val scrollState = rememberLazyListState()
|
val state by store.state.collectAsState()
|
||||||
|
val budget = state.selectedBudget?.let { id -> state.budgets?.first { it.id == id } }
|
||||||
TwigsScaffold(
|
TwigsScaffold(
|
||||||
store = store,
|
store = store,
|
||||||
title = "Categories",
|
title = budget?.name ?: "Select a Budget",
|
||||||
onClickFab = {
|
onClickFab = {
|
||||||
store.dispatch(CategoryAction.NewCategoryClicked)
|
store.dispatch(CategoryAction.NewCategoryClicked)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val state by store.state.collectAsState()
|
|
||||||
state.categories?.let { categories ->
|
state.categories?.let { categories ->
|
||||||
LazyColumn(
|
val categoryGroups = categories.groupByType()
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.padding(it),
|
.padding(it)
|
||||||
state = scrollState
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
items(categories, key = { c -> c.id!! }) { category ->
|
categoryGroups.toSortedMap().forEach { (group, c) ->
|
||||||
CategoryListItem(category, state.categoryBalances?.get(category.id!!)) {
|
Text(
|
||||||
store.dispatch(CategoryAction.SelectCategory(category.id))
|
modifier = Modifier.padding(8.dp),
|
||||||
|
text = group.capitalizedName,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Card {
|
||||||
|
c.forEach { category ->
|
||||||
|
CategoryListItem(category, state.categoryBalances?.get(category.id!!)) {
|
||||||
|
store.dispatch(CategoryAction.SelectCategory(category.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,20 +93,31 @@ fun CategoryListItem(category: Category, balance: Long?, onClick: (Category) ->
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = spacedBy(4.dp)
|
verticalArrangement = spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
Text(category.title, style = MaterialTheme.typography.bodyLarge)
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(category.title, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
balance?.let {
|
||||||
|
Text(
|
||||||
|
(category.amount - abs(it)).toCurrencyString() + " remaining",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
balance?.let {
|
balance?.let {
|
||||||
val denominator = remember { max(abs(it), abs(category.amount)).toFloat() }
|
val denominator = remember { max(abs(it), abs(category.amount)).toFloat() }
|
||||||
val progress =
|
val progress =
|
||||||
remember { if (denominator == 0f) 0f else abs(it).toFloat() / denominator }
|
remember { if (denominator == 0f) 0f else abs(it).toFloat() / denominator }
|
||||||
Log.d(
|
|
||||||
"Twigs",
|
|
||||||
"Category ${category.title} amount: $denominator balance: $it progress: $progress"
|
|
||||||
)
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(4.dp)),
|
||||||
progress = progress,
|
progress = progress,
|
||||||
color = if (category.expense) Color.Red else Color.Green
|
color = if (category.expense) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = Color.LightGray
|
||||||
)
|
)
|
||||||
} ?: LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
} ?: LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ fun CategoryForm(store: Store) {
|
||||||
val (description, setDescription) = remember { mutableStateOf(category.description ?: "") }
|
val (description, setDescription) = remember { mutableStateOf(category.description ?: "") }
|
||||||
val (amount, setAmount) = remember { mutableStateOf(category.amount.toDecimalString()) }
|
val (amount, setAmount) = remember { mutableStateOf(category.amount.toDecimalString()) }
|
||||||
val (expense, setExpense) = remember { mutableStateOf(category.expense) }
|
val (expense, setExpense) = remember { mutableStateOf(category.expense) }
|
||||||
|
val (archived, setArchived) = remember { mutableStateOf(category.archived) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
@ -94,6 +95,8 @@ fun CategoryForm(store: Store) {
|
||||||
setAmount = setAmount,
|
setAmount = setAmount,
|
||||||
expense = expense,
|
expense = expense,
|
||||||
setExpense = setExpense,
|
setExpense = setExpense,
|
||||||
|
archived = archived,
|
||||||
|
setArchived = setArchived
|
||||||
) {
|
) {
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
category.id?.let { id ->
|
category.id?.let { id ->
|
||||||
|
@ -103,12 +106,14 @@ fun CategoryForm(store: Store) {
|
||||||
description = description,
|
description = description,
|
||||||
amount = (amount.toDouble() * 100).toLong(),
|
amount = (amount.toDouble() * 100).toLong(),
|
||||||
expense = expense,
|
expense = expense,
|
||||||
|
archived = archived
|
||||||
)
|
)
|
||||||
} ?: CategoryAction.CreateCategory(
|
} ?: CategoryAction.CreateCategory(
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
amount = (amount.toDouble() * 100).toLong(),
|
amount = (amount.toDouble() * 100).toLong(),
|
||||||
expense = expense,
|
expense = expense,
|
||||||
|
archived = archived
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -127,6 +132,8 @@ fun CategoryForm(
|
||||||
setAmount: (String) -> Unit,
|
setAmount: (String) -> Unit,
|
||||||
expense: Boolean,
|
expense: Boolean,
|
||||||
setExpense: (Boolean) -> Unit,
|
setExpense: (Boolean) -> Unit,
|
||||||
|
archived: Boolean,
|
||||||
|
setArchived: (Boolean) -> Unit,
|
||||||
save: () -> Unit
|
save: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
@ -239,6 +246,15 @@ fun CategoryForm(
|
||||||
Text(text = "Income")
|
Text(text = "Income")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { setArchived(!archived) },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(checked = archived, onCheckedChange = { setArchived(!archived) })
|
||||||
|
Text("Archived")
|
||||||
|
}
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = save
|
onClick = save
|
||||||
|
|
|
@ -17,7 +17,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -109,7 +108,7 @@ fun RecurringTransactionDetails(
|
||||||
Text(
|
Text(
|
||||||
text = transaction.amount.toCurrencyString(),
|
text = transaction.amount.toCurrencyString(),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = if (transaction.expense) Color.Red else Color.Green
|
color = if (transaction.expense) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LabeledField("Description", transaction.description ?: "")
|
LabeledField("Description", transaction.description ?: "")
|
||||||
|
|
|
@ -5,7 +5,8 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
@ -17,7 +18,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
@ -34,41 +34,40 @@ import kotlinx.datetime.Clock
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RecurringTransactionsScreen(store: Store) {
|
fun RecurringTransactionsScreen(store: Store) {
|
||||||
|
val state by store.state.collectAsState()
|
||||||
|
val budget = state.selectedBudget?.let { id -> state.budgets?.first { it.id == id } }
|
||||||
TwigsScaffold(
|
TwigsScaffold(
|
||||||
store = store,
|
store = store,
|
||||||
title = "Recurring Transactions",
|
title = budget?.name ?: "Select a Budget",
|
||||||
onClickFab = {
|
onClickFab = {
|
||||||
store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val state by store.state.collectAsState()
|
|
||||||
state.recurringTransactions?.let { transactions ->
|
state.recurringTransactions?.let { transactions ->
|
||||||
val transactionGroups = remember { transactions.groupByStatus() }
|
val transactionGroups =
|
||||||
LazyColumn(
|
remember(state.editingRecurringTransaction) { transactions.groupByStatus() }
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(it)
|
.padding(it)
|
||||||
.padding(horizontal = 8.dp)
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
transactionGroups.forEach { (title, transactions) ->
|
transactionGroups.forEach { (title, transactions) ->
|
||||||
item(title) {
|
Text(
|
||||||
Text(
|
modifier = Modifier.padding(8.dp),
|
||||||
modifier = Modifier.padding(8.dp),
|
text = title,
|
||||||
text = title,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
fontWeight = FontWeight.Bold
|
||||||
fontWeight = FontWeight.Bold
|
)
|
||||||
)
|
Card {
|
||||||
}
|
transactions.forEach { transaction ->
|
||||||
item(transactions) {
|
RecurringTransactionListItem(transaction) {
|
||||||
Card {
|
store.dispatch(
|
||||||
transactions.forEach { transaction ->
|
RecurringTransactionAction.SelectRecurringTransaction(
|
||||||
RecurringTransactionListItem(transaction) {
|
transaction.id
|
||||||
store.dispatch(
|
|
||||||
RecurringTransactionAction.SelectRecurringTransaction(
|
|
||||||
transaction.id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +110,7 @@ fun RecurringTransactionListItem(
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
transaction.amount.toCurrencyString(),
|
transaction.amount.toCurrencyString(),
|
||||||
color = if (transaction.expense) Color.Red else Color.Green,
|
color = if (transaction.expense) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,16 @@ import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.remember
|
import androidx.compose.runtime.remember
|
||||||
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.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.wbrawner.budget.ui.TwigsScaffold
|
import com.wbrawner.budget.ui.TwigsScaffold
|
||||||
|
@ -46,6 +47,7 @@ fun TransactionDetailsScreen(store: Store) {
|
||||||
}
|
}
|
||||||
val category = state.categories?.firstOrNull { it.id == transaction.categoryId }
|
val category = state.categories?.firstOrNull { it.id == transaction.categoryId }
|
||||||
val budget = state.budgets!!.first { it.id == transaction.budgetId }
|
val budget = state.budgets!!.first { it.id == transaction.budgetId }
|
||||||
|
val (confirmDeletionShown, setConfirmDeletionShown) = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
TwigsScaffold(
|
TwigsScaffold(
|
||||||
store = store,
|
store = store,
|
||||||
|
@ -59,6 +61,9 @@ fun TransactionDetailsScreen(store: Store) {
|
||||||
IconButton({ store.dispatch(TransactionAction.EditTransaction(requireNotNull(transaction.id))) }) {
|
IconButton({ store.dispatch(TransactionAction.EditTransaction(requireNotNull(transaction.id))) }) {
|
||||||
Icon(Icons.Default.Edit, "Edit")
|
Icon(Icons.Default.Edit, "Edit")
|
||||||
}
|
}
|
||||||
|
IconButton({ setConfirmDeletionShown(true) }) {
|
||||||
|
Icon(Icons.Default.Delete, "Delete")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
TransactionDetails(
|
TransactionDetails(
|
||||||
|
@ -71,6 +76,35 @@ fun TransactionDetailsScreen(store: Store) {
|
||||||
if (state.editingTransaction) {
|
if (state.editingTransaction) {
|
||||||
TransactionFormDialog(store = store)
|
TransactionFormDialog(store = store)
|
||||||
}
|
}
|
||||||
|
if (confirmDeletionShown) {
|
||||||
|
AlertDialog(
|
||||||
|
text = {
|
||||||
|
Text("Are you sure you want to delete this transaction?")
|
||||||
|
},
|
||||||
|
onDismissRequest = { setConfirmDeletionShown(false) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
setConfirmDeletionShown(false)
|
||||||
|
store.dispatch(
|
||||||
|
TransactionAction.DeleteTransaction(
|
||||||
|
requireNotNull(
|
||||||
|
transaction.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
setConfirmDeletionShown(false)
|
||||||
|
}) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +141,7 @@ fun TransactionDetails(
|
||||||
Text(
|
Text(
|
||||||
text = transaction.amount.toCurrencyString(),
|
text = transaction.amount.toCurrencyString(),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = if (transaction.expense) Color.Red else Color.Green
|
color = if (transaction.expense) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LabeledField("Description", transaction.description ?: "")
|
LabeledField("Description", transaction.description ?: "")
|
||||||
|
|
|
@ -73,13 +73,18 @@ fun TransactionForm(store: Store) {
|
||||||
state.transactions?.first { it.id == state.selectedTransaction } ?: defaultTransaction
|
state.transactions?.first { it.id == state.selectedTransaction } ?: defaultTransaction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val (title, setTitle) = remember { mutableStateOf(transaction.title) }
|
val (title, setTitle) = remember(state.editingTransaction) { mutableStateOf(transaction.title) }
|
||||||
val (description, setDescription) = remember { mutableStateOf(transaction.description ?: "") }
|
val (description, setDescription) = remember(state.editingTransaction) {
|
||||||
val (date, setDate) = remember { mutableStateOf(transaction.date) }
|
mutableStateOf(
|
||||||
val (amount, setAmount) = remember { mutableStateOf(transaction.amount.toDecimalString()) }
|
transaction.description ?: ""
|
||||||
val (expense, setExpense) = remember { mutableStateOf(transaction.expense) }
|
)
|
||||||
val budget = remember { state.budgets!!.first { it.id == transaction.budgetId } }
|
}
|
||||||
val (category, setCategory) = remember { mutableStateOf(transaction.categoryId?.let { categoryId -> state.categories?.firstOrNull { it.id == categoryId } }) }
|
val (date, setDate) = remember(state.editingTransaction) { mutableStateOf(transaction.date) }
|
||||||
|
val (amount, setAmount) = remember(state.editingTransaction) { mutableStateOf(transaction.amount.toDecimalString()) }
|
||||||
|
val (expense, setExpense) = remember(state.editingTransaction) { mutableStateOf(transaction.expense) }
|
||||||
|
val budget =
|
||||||
|
remember(state.editingTransaction) { state.budgets!!.first { it.id == transaction.budgetId } }
|
||||||
|
val (category, setCategory) = remember(state.editingTransaction) { mutableStateOf(transaction.categoryId?.let { categoryId -> state.categories?.firstOrNull { it.id == categoryId } }) }
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
|
|
|
@ -5,7 +5,8 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
@ -17,7 +18,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
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.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
@ -36,37 +36,36 @@ import java.text.NumberFormat
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TransactionsScreen(store: Store) {
|
fun TransactionsScreen(store: Store) {
|
||||||
|
val state by store.state.collectAsState()
|
||||||
|
val budget = state.selectedBudget?.let { id -> state.budgets?.first { it.id == id } }
|
||||||
TwigsScaffold(
|
TwigsScaffold(
|
||||||
store = store,
|
store = store,
|
||||||
title = "Transactions",
|
title = budget?.name ?: "Select a Budget",
|
||||||
onClickFab = {
|
onClickFab = {
|
||||||
store.dispatch(TransactionAction.NewTransactionClicked)
|
store.dispatch(TransactionAction.NewTransactionClicked)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val state by store.state.collectAsState()
|
|
||||||
state.transactions?.let { transactions ->
|
state.transactions?.let { transactions ->
|
||||||
val transactionGroups = remember { transactions.groupByDate() }
|
val transactionGroups =
|
||||||
LazyColumn(
|
remember(state.editingTransaction) { transactions.groupByDate() }
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(it)
|
.padding(it)
|
||||||
.padding(horizontal = 8.dp)
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(start = 8.dp, end = 8.dp, bottom = 8.dp)
|
||||||
) {
|
) {
|
||||||
transactionGroups.forEach { (timestamp, transactions) ->
|
transactionGroups.forEach { (timestamp, transactions) ->
|
||||||
item(timestamp) {
|
Text(
|
||||||
Text(
|
modifier = Modifier.padding(8.dp),
|
||||||
modifier = Modifier.padding(8.dp),
|
text = timestamp.toInstant().format(LocalContext.current),
|
||||||
text = timestamp.toInstant().format(LocalContext.current),
|
style = MaterialTheme.typography.titleSmall,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
fontWeight = FontWeight.Bold
|
||||||
fontWeight = FontWeight.Bold
|
)
|
||||||
)
|
Card {
|
||||||
}
|
transactions.forEach { transaction ->
|
||||||
item(transactions) {
|
TransactionListItem(transaction) {
|
||||||
Card {
|
store.dispatch(TransactionAction.SelectTransaction(transaction.id))
|
||||||
transactions.forEach { transaction ->
|
|
||||||
TransactionListItem(transaction) {
|
|
||||||
store.dispatch(TransactionAction.SelectTransaction(transaction.id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +105,7 @@ fun TransactionListItem(transaction: Transaction, onClick: (Transaction) -> Unit
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
transaction.amount.toCurrencyString(),
|
transaction.amount.toCurrencyString(),
|
||||||
color = if (transaction.expense) Color.Red else Color.Green,
|
color = if (transaction.expense) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
sealed class Route(val path: String) {
|
sealed class Route(val path: String) {
|
||||||
object Welcome : Route("welcome")
|
object Welcome : Route("welcome")
|
||||||
|
@ -36,6 +37,10 @@ data class State(
|
||||||
val user: User? = null,
|
val user: User? = null,
|
||||||
val budgets: List<Budget>? = null,
|
val budgets: List<Budget>? = null,
|
||||||
val budgetBalance: Long? = null,
|
val budgetBalance: Long? = null,
|
||||||
|
val actualIncome: Long? = null,
|
||||||
|
val actualExpenses: Long? = null,
|
||||||
|
val expectedIncome: Long? = null,
|
||||||
|
val expectedExpenses: Long? = null,
|
||||||
val selectedBudget: String? = null,
|
val selectedBudget: String? = null,
|
||||||
val editingBudget: Boolean = false,
|
val editingBudget: Boolean = false,
|
||||||
val categories: List<Category>? = null,
|
val categories: List<Category>? = null,
|
||||||
|
@ -46,6 +51,8 @@ data class State(
|
||||||
val selectedTransaction: String? = null,
|
val selectedTransaction: String? = null,
|
||||||
val selectedTransactionCreatedBy: User? = null,
|
val selectedTransactionCreatedBy: User? = null,
|
||||||
val editingTransaction: Boolean = false,
|
val editingTransaction: Boolean = false,
|
||||||
|
val from: Instant = startOfMonth(),
|
||||||
|
val to: Instant = endOfMonth(),
|
||||||
val recurringTransactions: List<RecurringTransaction>? = null,
|
val recurringTransactions: List<RecurringTransaction>? = null,
|
||||||
val selectedRecurringTransaction: String? = null,
|
val selectedRecurringTransaction: String? = null,
|
||||||
val selectedRecurringTransactionCreatedBy: User? = null,
|
val selectedRecurringTransactionCreatedBy: User? = null,
|
||||||
|
@ -53,11 +60,7 @@ data class State(
|
||||||
val loading: Boolean = false,
|
val loading: Boolean = false,
|
||||||
val route: Route = Route.Login,
|
val route: Route = Route.Login,
|
||||||
val initialRoute: Route = Route.Login
|
val initialRoute: Route = Route.Login
|
||||||
) {
|
)
|
||||||
override fun toString(): String {
|
|
||||||
return "State(recurringTransactionsSize=${recurringTransactions?.size}, route=$route)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
object AboutClicked : Action
|
object AboutClicked : Action
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.wbrawner.twigs.shared.Reducer
|
||||||
import com.wbrawner.twigs.shared.Route
|
import com.wbrawner.twigs.shared.Route
|
||||||
import com.wbrawner.twigs.shared.State
|
import com.wbrawner.twigs.shared.State
|
||||||
import com.wbrawner.twigs.shared.replace
|
import com.wbrawner.twigs.shared.replace
|
||||||
|
import com.wbrawner.twigs.shared.transaction.TransactionAction
|
||||||
import com.wbrawner.twigs.shared.user.ConfigAction
|
import com.wbrawner.twigs.shared.user.ConfigAction
|
||||||
import com.wbrawner.twigs.shared.user.UserPermission
|
import com.wbrawner.twigs.shared.user.UserPermission
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -127,6 +128,17 @@ class BudgetReducer(
|
||||||
|
|
||||||
is BudgetAction.BudgetSelected -> state().copy(selectedBudget = action.id)
|
is BudgetAction.BudgetSelected -> state().copy(selectedBudget = action.id)
|
||||||
|
|
||||||
|
is TransactionAction.LoadTransactionsSuccess -> {
|
||||||
|
val balance = action.transactions.sumOf {
|
||||||
|
if (it.expense) {
|
||||||
|
it.amount * -1
|
||||||
|
} else {
|
||||||
|
it.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state().copy(budgetBalance = balance)
|
||||||
|
}
|
||||||
|
|
||||||
// is BudgetAction.UpdateBudget -> state.copy(loading = true).also {
|
// is BudgetAction.UpdateBudget -> state.copy(loading = true).also {
|
||||||
// dispatch(action.async())
|
// dispatch(action.async())
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -2,9 +2,10 @@ package com.wbrawner.twigs.shared.budget
|
||||||
|
|
||||||
import com.wbrawner.twigs.shared.Repository
|
import com.wbrawner.twigs.shared.Repository
|
||||||
import com.wbrawner.twigs.shared.network.APIService
|
import com.wbrawner.twigs.shared.network.APIService
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
interface BudgetRepository : Repository<Budget> {
|
interface BudgetRepository : Repository<Budget> {
|
||||||
suspend fun getBalance(id: String): Long
|
suspend fun getBalance(id: String, from: Instant, to: Instant): Long
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkBudgetRepository(private val apiService: APIService) : BudgetRepository {
|
class NetworkBudgetRepository(private val apiService: APIService) : BudgetRepository {
|
||||||
|
@ -19,6 +20,6 @@ class NetworkBudgetRepository(private val apiService: APIService) : BudgetReposi
|
||||||
|
|
||||||
override suspend fun delete(id: String) = apiService.deleteBudget(id)
|
override suspend fun delete(id: String) = apiService.deleteBudget(id)
|
||||||
|
|
||||||
override suspend fun getBalance(id: String): Long =
|
override suspend fun getBalance(id: String, from: Instant, to: Instant): Long =
|
||||||
apiService.sumTransactions(budgetId = id).balance
|
apiService.sumTransactions(budgetId = id, from = from, to = to).balance
|
||||||
}
|
}
|
|
@ -6,13 +6,16 @@ import com.wbrawner.twigs.shared.Route
|
||||||
import com.wbrawner.twigs.shared.State
|
import com.wbrawner.twigs.shared.State
|
||||||
import com.wbrawner.twigs.shared.budget.BudgetAction
|
import com.wbrawner.twigs.shared.budget.BudgetAction
|
||||||
import com.wbrawner.twigs.shared.replace
|
import com.wbrawner.twigs.shared.replace
|
||||||
|
import com.wbrawner.twigs.shared.transaction.TransactionAction
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
sealed interface CategoryAction : Action {
|
sealed interface CategoryAction : Action {
|
||||||
object CategoriesClicked : CategoryAction
|
object CategoriesClicked : CategoryAction
|
||||||
data class BalancesCalculated(
|
data class BalancesCalculated(
|
||||||
val budgetBalance: Long,
|
val categoryBalances: Map<String, Long>,
|
||||||
val categoryBalances: Map<String, Long>
|
val actualIncome: Long,
|
||||||
|
val actualExpenses: Long
|
||||||
) : CategoryAction
|
) : CategoryAction
|
||||||
|
|
||||||
data class LoadCategoriesSuccess(val categories: List<Category>) : CategoryAction
|
data class LoadCategoriesSuccess(val categories: List<Category>) : CategoryAction
|
||||||
|
@ -24,7 +27,8 @@ sealed interface CategoryAction : Action {
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
val expense: Boolean
|
val expense: Boolean,
|
||||||
|
val archived: Boolean,
|
||||||
) : CategoryAction
|
) : CategoryAction
|
||||||
|
|
||||||
data class SaveCategorySuccess(val category: Category) : CategoryAction
|
data class SaveCategorySuccess(val category: Category) : CategoryAction
|
||||||
|
@ -35,6 +39,7 @@ sealed interface CategoryAction : Action {
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
val expense: Boolean,
|
val expense: Boolean,
|
||||||
|
val archived: Boolean,
|
||||||
val error: Exception
|
val error: Exception
|
||||||
) : CategoryAction
|
) : CategoryAction
|
||||||
|
|
||||||
|
@ -48,6 +53,7 @@ sealed interface CategoryAction : Action {
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val amount: Long,
|
val amount: Long,
|
||||||
val expense: Boolean,
|
val expense: Boolean,
|
||||||
|
val archived: Boolean,
|
||||||
) : CategoryAction
|
) : CategoryAction
|
||||||
|
|
||||||
data class DeleteCategory(val id: String) : CategoryAction
|
data class DeleteCategory(val id: String) : CategoryAction
|
||||||
|
@ -78,7 +84,14 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
|
||||||
dispatch(CategoryAction.LoadCategoriesFailed(e))
|
dispatch(CategoryAction.LoadCategoriesFailed(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state().copy(categories = null)
|
state().copy(
|
||||||
|
categories = null,
|
||||||
|
selectedCategory = null,
|
||||||
|
editingCategory = false,
|
||||||
|
categoryBalances = null,
|
||||||
|
actualIncome = null,
|
||||||
|
actualExpenses = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CategoryAction.SelectCategory -> state().copy(
|
is CategoryAction.SelectCategory -> state().copy(
|
||||||
|
@ -86,23 +99,69 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
|
||||||
route = Route.Categories(action.id)
|
route = Route.Categories(action.id)
|
||||||
).also { newState -> println("Category selected state update: $newState") }
|
).also { newState -> println("Category selected state update: $newState") }
|
||||||
|
|
||||||
is CategoryAction.LoadCategoriesSuccess -> state().copy(categories = action.categories)
|
is CategoryAction.LoadCategoriesSuccess -> {
|
||||||
|
var expectedIncome = 0L
|
||||||
|
var expectedExpenses = 0L
|
||||||
|
action.categories.forEach { category ->
|
||||||
|
if (category.archived) return@forEach
|
||||||
|
if (category.expense) {
|
||||||
|
expectedExpenses += category.amount
|
||||||
|
} else {
|
||||||
|
expectedIncome += category.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentState = state()
|
||||||
|
val defaultCategoryBalances =
|
||||||
|
action.categories.associate { it.id!! to 0L }.toMutableMap()
|
||||||
|
val categoryBalances: Map<String, Long>? = currentState.categoryBalances?.let {
|
||||||
|
defaultCategoryBalances.apply {
|
||||||
|
putAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state().copy(
|
||||||
|
categories = action.categories,
|
||||||
|
categoryBalances = categoryBalances,
|
||||||
|
expectedExpenses = expectedExpenses,
|
||||||
|
expectedIncome = expectedIncome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TransactionAction.LoadTransactionsSuccess -> state()
|
||||||
.also {
|
.also {
|
||||||
launch {
|
launch {
|
||||||
var budgetBalance = 0L
|
val categoryBalances =
|
||||||
val categoryBalances = mutableMapOf<String, Long>()
|
it.categories?.associate { it.id!! to 0L }?.toMutableMap()
|
||||||
action.categories.forEach { category ->
|
?: mutableMapOf()
|
||||||
val balance = categoryRepository.getBalance(category.id!!)
|
var actualIncome = 0L
|
||||||
categoryBalances[category.id] = balance
|
var actualExpenses = 0L
|
||||||
budgetBalance += balance
|
action.transactions.forEach { transaction ->
|
||||||
|
val category = transaction.categoryId
|
||||||
|
var balance = category?.let { categoryBalances[it] ?: 0L } ?: 0L
|
||||||
|
if (transaction.expense) {
|
||||||
|
balance -= transaction.amount
|
||||||
|
actualExpenses += abs(transaction.amount)
|
||||||
|
} else {
|
||||||
|
balance += transaction.amount
|
||||||
|
actualIncome += transaction.amount
|
||||||
|
}
|
||||||
|
category?.let {
|
||||||
|
categoryBalances[it] = balance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dispatch(CategoryAction.BalancesCalculated(budgetBalance, categoryBalances))
|
dispatch(
|
||||||
|
CategoryAction.BalancesCalculated(
|
||||||
|
categoryBalances,
|
||||||
|
actualIncome = actualIncome,
|
||||||
|
actualExpenses = actualExpenses
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is CategoryAction.BalancesCalculated -> state().copy(
|
is CategoryAction.BalancesCalculated -> state().copy(
|
||||||
budgetBalance = action.budgetBalance,
|
categoryBalances = action.categoryBalances,
|
||||||
categoryBalances = action.categoryBalances
|
actualExpenses = action.actualExpenses,
|
||||||
|
actualIncome = action.actualIncome
|
||||||
)
|
)
|
||||||
|
|
||||||
is CategoryAction.CancelEditCategory -> state().copy(editingCategory = false)
|
is CategoryAction.CancelEditCategory -> state().copy(editingCategory = false)
|
||||||
|
@ -162,4 +221,27 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
|
||||||
|
|
||||||
else -> state()
|
else -> state()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CategoryGroup {
|
||||||
|
INCOME,
|
||||||
|
EXPENSE,
|
||||||
|
ARCHIVED
|
||||||
|
}
|
||||||
|
|
||||||
|
val Category.group: CategoryGroup
|
||||||
|
get() = when {
|
||||||
|
archived -> CategoryGroup.ARCHIVED
|
||||||
|
expense -> CategoryGroup.EXPENSE
|
||||||
|
else -> CategoryGroup.INCOME
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<Category>.groupByType(): Map<CategoryGroup, List<Category>> {
|
||||||
|
val groups = mutableMapOf<CategoryGroup, List<Category>>()
|
||||||
|
forEach { category ->
|
||||||
|
val list = groups[category.group]?.toMutableList() ?: mutableListOf()
|
||||||
|
list.add(category)
|
||||||
|
groups[category.group] = list.sortedBy { it.title }
|
||||||
|
}
|
||||||
|
return groups
|
||||||
}
|
}
|
|
@ -2,10 +2,11 @@ package com.wbrawner.twigs.shared.category
|
||||||
|
|
||||||
import com.wbrawner.twigs.shared.Repository
|
import com.wbrawner.twigs.shared.Repository
|
||||||
import com.wbrawner.twigs.shared.network.APIService
|
import com.wbrawner.twigs.shared.network.APIService
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
interface CategoryRepository : Repository<Category> {
|
interface CategoryRepository : Repository<Category> {
|
||||||
suspend fun findAll(budgetIds: Array<String>? = null): List<Category>
|
suspend fun findAll(budgetIds: Array<String>? = null): List<Category>
|
||||||
suspend fun getBalance(id: String): Long
|
suspend fun getBalance(id: String, from: Instant, to: Instant): Long
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkCategoryRepository(private val apiService: APIService) : CategoryRepository {
|
class NetworkCategoryRepository(private val apiService: APIService) : CategoryRepository {
|
||||||
|
@ -14,8 +15,8 @@ class NetworkCategoryRepository(private val apiService: APIService) : CategoryRe
|
||||||
|
|
||||||
override suspend fun findAll(): List<Category> = findAll(null)
|
override suspend fun findAll(): List<Category> = findAll(null)
|
||||||
|
|
||||||
override suspend fun getBalance(id: String): Long =
|
override suspend fun getBalance(id: String, from: Instant, to: Instant): Long =
|
||||||
apiService.sumTransactions(categoryId = id).balance
|
apiService.sumTransactions(categoryId = id, from = from, to = to).balance
|
||||||
|
|
||||||
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
|
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import io.ktor.client.engine.HttpClientEngineConfig
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
const val BASE_PATH = "/api"
|
const val BASE_PATH = "/api"
|
||||||
|
@ -40,7 +41,7 @@ interface APIService {
|
||||||
|
|
||||||
suspend fun getCategories(
|
suspend fun getCategories(
|
||||||
budgetIds: Array<String>? = null,
|
budgetIds: Array<String>? = null,
|
||||||
archived: Boolean? = false,
|
archived: Boolean? = null,
|
||||||
count: Int? = null,
|
count: Int? = null,
|
||||||
page: Int? = null,
|
page: Int? = null,
|
||||||
): List<Category>
|
): List<Category>
|
||||||
|
@ -74,10 +75,10 @@ interface APIService {
|
||||||
suspend fun deleteRecurringTransaction(id: String)
|
suspend fun deleteRecurringTransaction(id: String)
|
||||||
|
|
||||||
suspend fun getTransactions(
|
suspend fun getTransactions(
|
||||||
|
from: Instant,
|
||||||
|
to: Instant,
|
||||||
budgetIds: List<String>? = null,
|
budgetIds: List<String>? = null,
|
||||||
categoryIds: List<String>? = null,
|
categoryIds: List<String>? = null,
|
||||||
from: String? = null,
|
|
||||||
to: String? = null,
|
|
||||||
count: Int? = null,
|
count: Int? = null,
|
||||||
page: Int? = null
|
page: Int? = null
|
||||||
): List<Transaction>
|
): List<Transaction>
|
||||||
|
@ -85,8 +86,10 @@ interface APIService {
|
||||||
suspend fun getTransaction(id: String): Transaction
|
suspend fun getTransaction(id: String): Transaction
|
||||||
|
|
||||||
suspend fun sumTransactions(
|
suspend fun sumTransactions(
|
||||||
|
from: Instant,
|
||||||
|
to: Instant,
|
||||||
budgetId: String? = null,
|
budgetId: String? = null,
|
||||||
categoryId: String? = null
|
categoryId: String? = null,
|
||||||
): BalanceResponse
|
): BalanceResponse
|
||||||
|
|
||||||
suspend fun newTransaction(transaction: Transaction): Transaction
|
suspend fun newTransaction(transaction: Transaction): Transaction
|
||||||
|
@ -122,7 +125,7 @@ interface APIService {
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T: HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
fun <T : HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
|
|
|
@ -21,6 +21,7 @@ import io.ktor.http.HttpMethod
|
||||||
import io.ktor.http.URLBuilder
|
import io.ktor.http.URLBuilder
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.http.path
|
import io.ktor.http.path
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
class KtorAPIService(
|
class KtorAPIService(
|
||||||
private val client: HttpClient
|
private val client: HttpClient
|
||||||
|
@ -84,10 +85,10 @@ class KtorAPIService(
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getTransactions(
|
override suspend fun getTransactions(
|
||||||
|
from: Instant,
|
||||||
|
to: Instant,
|
||||||
budgetIds: List<String>?,
|
budgetIds: List<String>?,
|
||||||
categoryIds: List<String>?,
|
categoryIds: List<String>?,
|
||||||
from: String?,
|
|
||||||
to: String?,
|
|
||||||
count: Int?,
|
count: Int?,
|
||||||
page: Int?
|
page: Int?
|
||||||
): List<Transaction> = request(
|
): List<Transaction> = request(
|
||||||
|
@ -95,8 +96,8 @@ class KtorAPIService(
|
||||||
queryParams = listOf(
|
queryParams = listOf(
|
||||||
"budgetIds" to budgetIds,
|
"budgetIds" to budgetIds,
|
||||||
"categoryIds" to categoryIds,
|
"categoryIds" to categoryIds,
|
||||||
"from" to from,
|
"from" to from.toString(),
|
||||||
"to" to to,
|
"to" to to.toString(),
|
||||||
"count" to count,
|
"count" to count,
|
||||||
"page" to page,
|
"page" to page,
|
||||||
)
|
)
|
||||||
|
@ -104,12 +105,19 @@ class KtorAPIService(
|
||||||
|
|
||||||
override suspend fun getTransaction(id: String): Transaction = request("transactions/$id")
|
override suspend fun getTransaction(id: String): Transaction = request("transactions/$id")
|
||||||
|
|
||||||
override suspend fun sumTransactions(budgetId: String?, categoryId: String?): BalanceResponse =
|
override suspend fun sumTransactions(
|
||||||
|
from: Instant,
|
||||||
|
to: Instant,
|
||||||
|
budgetId: String?,
|
||||||
|
categoryId: String?
|
||||||
|
): BalanceResponse =
|
||||||
request(
|
request(
|
||||||
path = "transactions/sum",
|
path = "transactions/sum",
|
||||||
queryParams = listOf(
|
queryParams = listOf(
|
||||||
"budgetId" to budgetId,
|
"budgetId" to budgetId,
|
||||||
"categoryId" to categoryId
|
"categoryId" to categoryId,
|
||||||
|
"from" to from.toString(),
|
||||||
|
"to" to to.toString(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ sealed class Frequency {
|
||||||
override fun toString(): String = "M;$count;$dayOfMonth;$time"
|
override fun toString(): String = "M;$count;$dayOfMonth;$time"
|
||||||
override val name: String = "Monthly"
|
override val name: String = "Monthly"
|
||||||
override val description: String
|
override val description: String
|
||||||
get() = if (count == 1) "Every month on ${dayOfMonth.description}"
|
get() = if (count == 1) "Every month on the ${dayOfMonth.description}"
|
||||||
else "Every $count months on ${dayOfMonth.description}"
|
else "Every $count months on ${dayOfMonth.description}"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -163,8 +163,7 @@ sealed class DayOfMonth {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FixedDayOfMonth(val day: Int) : DayOfMonth() {
|
data class FixedDayOfMonth(val day: Int) : DayOfMonth() {
|
||||||
override val description: String
|
override val description: String = day.ordinalString
|
||||||
get() = "$day"
|
|
||||||
|
|
||||||
override fun toString(): String = "DAY-$day"
|
override fun toString(): String = "DAY-$day"
|
||||||
}
|
}
|
||||||
|
@ -200,7 +199,7 @@ val Enum<*>.capitalizedName: String
|
||||||
class DayOfYear private constructor(val month: Int, val day: Int) {
|
class DayOfYear private constructor(val month: Int, val day: Int) {
|
||||||
|
|
||||||
val description: String
|
val description: String
|
||||||
get() = "${month.toMonth().capitalizedName} $day"
|
get() = "${month.toMonth().capitalizedName} ${day.ordinalString}"
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "${month.padStart(2, '0')}-${day.padStart(2, '0')}"
|
return "${month.padStart(2, '0')}-${day.padStart(2, '0')}"
|
||||||
|
@ -228,6 +227,14 @@ class DayOfYear private constructor(val month: Int, val day: Int) {
|
||||||
|
|
||||||
fun Int.toMonth(): Month = Month(this)
|
fun Int.toMonth(): Month = Month(this)
|
||||||
|
|
||||||
|
val Int.ordinalString: String
|
||||||
|
get() = when {
|
||||||
|
mod(1) == 0 -> "${this}st"
|
||||||
|
mod(2) == 0 -> "${this}nd"
|
||||||
|
mod(3) == 0 -> "${this}rd"
|
||||||
|
else -> "${this}th"
|
||||||
|
}
|
||||||
|
|
||||||
data class Time(val hours: Int, val minutes: Int, val seconds: Int) {
|
data class Time(val hours: Int, val minutes: Int, val seconds: Int) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val s = StringBuilder()
|
val s = StringBuilder()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.wbrawner.twigs.shared.transaction
|
package com.wbrawner.twigs.shared.transaction
|
||||||
|
|
||||||
import com.wbrawner.twigs.shared.Action
|
import com.wbrawner.twigs.shared.Action
|
||||||
|
import com.wbrawner.twigs.shared.Effect
|
||||||
import com.wbrawner.twigs.shared.Reducer
|
import com.wbrawner.twigs.shared.Reducer
|
||||||
import com.wbrawner.twigs.shared.Route
|
import com.wbrawner.twigs.shared.Route
|
||||||
import com.wbrawner.twigs.shared.State
|
import com.wbrawner.twigs.shared.State
|
||||||
|
@ -25,6 +26,7 @@ sealed interface TransactionAction : Action {
|
||||||
object TransactionsClicked : TransactionAction
|
object TransactionsClicked : TransactionAction
|
||||||
data class LoadTransactionsSuccess(val transactions: List<Transaction>) : TransactionAction
|
data class LoadTransactionsSuccess(val transactions: List<Transaction>) : TransactionAction
|
||||||
data class LoadTransactionsFailed(val error: Exception) : TransactionAction
|
data class LoadTransactionsFailed(val error: Exception) : TransactionAction
|
||||||
|
data class ChangeDateRange(val from: Instant, val to: Instant) : TransactionAction
|
||||||
object NewTransactionClicked : TransactionAction
|
object NewTransactionClicked : TransactionAction
|
||||||
data class CreateTransaction(
|
data class CreateTransaction(
|
||||||
val title: String,
|
val title: String,
|
||||||
|
@ -67,6 +69,10 @@ sealed interface TransactionAction : Action {
|
||||||
) : TransactionAction
|
) : TransactionAction
|
||||||
|
|
||||||
data class DeleteTransaction(val id: String) : TransactionAction
|
data class DeleteTransaction(val id: String) : TransactionAction
|
||||||
|
|
||||||
|
data class TransactionDeleted(val id: String) : TransactionAction
|
||||||
|
|
||||||
|
data class TransactionDeletedFailure(val id: String) : TransactionAction
|
||||||
}
|
}
|
||||||
|
|
||||||
class TransactionReducer(
|
class TransactionReducer(
|
||||||
|
@ -89,6 +95,24 @@ class TransactionReducer(
|
||||||
|
|
||||||
is TransactionAction.TransactionsClicked -> state().copy(route = Route.Transactions(null))
|
is TransactionAction.TransactionsClicked -> state().copy(route = Route.Transactions(null))
|
||||||
is TransactionAction.LoadTransactionsSuccess -> state().copy(transactions = action.transactions)
|
is TransactionAction.LoadTransactionsSuccess -> state().copy(transactions = action.transactions)
|
||||||
|
is TransactionAction.ChangeDateRange -> state().copy(
|
||||||
|
from = action.from,
|
||||||
|
to = action.to
|
||||||
|
).also {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
val transactions = transactionRepository.findAll(
|
||||||
|
budgetIds = listOf(it.selectedBudget!!),
|
||||||
|
start = it.from,
|
||||||
|
end = it.to,
|
||||||
|
)
|
||||||
|
dispatch(TransactionAction.LoadTransactionsSuccess(transactions))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
dispatch(TransactionAction.LoadTransactionsFailed(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is TransactionAction.NewTransactionClicked -> state().copy(editingTransaction = true)
|
is TransactionAction.NewTransactionClicked -> state().copy(editingTransaction = true)
|
||||||
is TransactionAction.CancelEditTransaction -> {
|
is TransactionAction.CancelEditTransaction -> {
|
||||||
val currentState = state()
|
val currentState = state()
|
||||||
|
@ -147,25 +171,30 @@ class TransactionReducer(
|
||||||
val transactions = currentState.transactions?.toMutableList() ?: mutableListOf()
|
val transactions = currentState.transactions?.toMutableList() ?: mutableListOf()
|
||||||
transactions.replace(action.transaction)
|
transactions.replace(action.transaction)
|
||||||
transactions.sortByDescending { it.date }
|
transactions.sortByDescending { it.date }
|
||||||
|
dispatch(TransactionAction.LoadTransactionsSuccess(transactions))
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
loading = false,
|
loading = false,
|
||||||
transactions = transactions.toList(),
|
transactions = transactions.toList(),
|
||||||
selectedTransaction = action.transaction.id,
|
selectedTransaction = action.transaction.id,
|
||||||
selectedTransactionCreatedBy = currentState.user,
|
selectedTransactionCreatedBy = currentState.user,
|
||||||
|
route = Route.Transactions(action.transaction.id),
|
||||||
editingTransaction = false
|
editingTransaction = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BudgetAction.BudgetSelected -> {
|
is BudgetAction.BudgetSelected -> state().copy(transactions = null).also {
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val transactions = transactionRepository.findAll(budgetIds = listOf(action.id))
|
val transactions = transactionRepository.findAll(
|
||||||
|
start = it.from,
|
||||||
|
end = it.to,
|
||||||
|
budgetIds = listOf(action.id)
|
||||||
|
)
|
||||||
dispatch(TransactionAction.LoadTransactionsSuccess(transactions))
|
dispatch(TransactionAction.LoadTransactionsSuccess(transactions))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
dispatch(TransactionAction.LoadTransactionsFailed(e))
|
dispatch(TransactionAction.LoadTransactionsFailed(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state().copy(transactions = null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ConfigAction.Logout -> state().copy(
|
is ConfigAction.Logout -> state().copy(
|
||||||
|
@ -198,6 +227,32 @@ class TransactionReducer(
|
||||||
selectedTransactionCreatedBy = action.createdBy
|
selectedTransactionCreatedBy = action.createdBy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is TransactionAction.DeleteTransaction -> state().copy(loading = true).also {
|
||||||
|
launch {
|
||||||
|
try {
|
||||||
|
transactionRepository.delete(action.id)
|
||||||
|
dispatch(TransactionAction.TransactionDeleted(action.id))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
dispatch(TransactionAction.TransactionDeletedFailure(action.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is TransactionAction.TransactionDeleted -> {
|
||||||
|
val currentState = state()
|
||||||
|
currentState.copy(
|
||||||
|
transactions = currentState.transactions?.filter { it.id != action.id },
|
||||||
|
selectedTransaction = if (currentState.selectedTransaction == action.id) null else currentState.selectedTransaction,
|
||||||
|
editingTransaction = if (currentState.selectedTransaction == action.id) false else currentState.editingTransaction,
|
||||||
|
route = if (currentState.selectedTransaction == action.id) Route.Transactions() else currentState.route,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TransactionAction.TransactionDeletedFailure -> state().also {
|
||||||
|
emit(Effect.Error("Failed to delete transaction"))
|
||||||
|
}
|
||||||
|
|
||||||
else -> state()
|
else -> state()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ interface TransactionRepository : Repository<Transaction> {
|
||||||
suspend fun findAll(
|
suspend fun findAll(
|
||||||
budgetIds: List<String>? = null,
|
budgetIds: List<String>? = null,
|
||||||
categoryIds: List<String>? = null,
|
categoryIds: List<String>? = null,
|
||||||
start: Instant? = startOfMonth(),
|
start: Instant,
|
||||||
end: Instant? = endOfMonth()
|
end: Instant
|
||||||
): List<Transaction>
|
): List<Transaction>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,16 +19,21 @@ class NetworkTransactionRepository(private val apiService: APIService) : Transac
|
||||||
override suspend fun findAll(
|
override suspend fun findAll(
|
||||||
budgetIds: List<String>?,
|
budgetIds: List<String>?,
|
||||||
categoryIds: List<String>?,
|
categoryIds: List<String>?,
|
||||||
start: Instant?,
|
start: Instant,
|
||||||
end: Instant?
|
end: Instant
|
||||||
): List<Transaction> = apiService.getTransactions(
|
): List<Transaction> = apiService.getTransactions(
|
||||||
budgetIds,
|
budgetIds = budgetIds,
|
||||||
categoryIds,
|
categoryIds = categoryIds,
|
||||||
from = start.toString(),
|
from = start,
|
||||||
to = end.toString()
|
to = end
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun findAll(): List<Transaction> = findAll(null, null)
|
override suspend fun findAll(): List<Transaction> = findAll(
|
||||||
|
start = startOfMonth(),
|
||||||
|
end = endOfMonth(),
|
||||||
|
budgetIds = null,
|
||||||
|
categoryIds = null
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun create(newItem: Transaction): Transaction =
|
override suspend fun create(newItem: Transaction): Transaction =
|
||||||
apiService.newTransaction(newItem)
|
apiService.newTransaction(newItem)
|
||||||
|
|
Loading…
Reference in a new issue