Compare commits

...

8 Commits

13 changed files with 571 additions and 127 deletions

View File

@ -1,23 +1,13 @@
package com.hyperling.expensetracker
/*class Expense {
var name: String = ""
var cost: Double = 0.0
var freq: Char = '*'
var note: String = ""
import androidx.room.Entity
import androidx.room.PrimaryKey
/*public Expense (val name: String, val cost: Double, val freq: Char, val note: String) {
this.name = name
this.name = name
this.name = name
this.name = name
}*/
}*/
class Expense (
var name: String = "",
var cost: Double = 0.0,
var freq: Char = '*',
var note: String = "",
) {
}
@Entity
data class Expense (
val name: String,
val cost: String,
val rate: Enum<Rate>,
@PrimaryKey(autoGenerate = true)
val ID: Int = 0,
)

View File

@ -0,0 +1,30 @@
package com.hyperling.expensetracker
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface ExpenseDao {
// 2024-12-28
// If these complain about return type errors, try upgrading
// the ROOM version, otherwise remove the SUSPEND keyword.
@Upsert
suspend fun upsertExpense(expense: Expense)
@Delete
suspend fun deleteExpense(expense: Expense)
@Query("SELECT * FROM expense ORDER BY name ASC")
fun selectExpensesName(): Flow<List<Expense>>
@Query("SELECT * FROM expense ORDER BY rate ASC")
fun selectExpensesRate(): Flow<List<Expense>>
@Query("SELECT * FROM expense ORDER BY cost ASC")
fun selectExpensesCost(): Flow<List<Expense>>
}

View File

@ -0,0 +1,12 @@
package com.hyperling.expensetracker
import androidx.room.Database
import androidx.room.RoomDatabase
@Database (
entities = [Expense::class],
version = 1
)
abstract class ExpenseDatabase: RoomDatabase() {
abstract val dao: ExpenseDao
}

View File

@ -0,0 +1,82 @@
package com.hyperling.expensetracker
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.unit.dp
@Composable
fun ExpenseDialogAdd(
state: ExpenseState,
onEvent: (ExpenseEvent) -> Unit,
modifier: Modifier = Modifier
) {
AlertDialog(
onDismissRequest = {
onEvent(ExpenseEvent.HideDialog)
},
title = { Text(text = "Add Expense") },
text = {
Column (
verticalArrangement = Arrangement.spacedBy(8.dp)
){
TextField(
value = state.name,
onValueChange = {
onEvent(ExpenseEvent.SetName(it))
},
placeholder = {
Text(text = "Name")
},
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words)
)
// TBD: Had to make this a String, can we turn it back to a Double somehow?
TextField(
value = state.cost,
onValueChange = {
onEvent(ExpenseEvent.SetCost(it))
},
placeholder = {
Text(text = "Cost")
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal)
)
/* Unsure what to do here yet, a simple Picker type does not seems to exist?
TextField(
value = state.rate,
onValueChange = {
onEvent(ExpenseEvent.SetRate(it))
},
placeholder = {
Text(text = "Rate")
},
)
*/
}
},
confirmButton = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Button(onClick = {
onEvent(ExpenseEvent.SaveExpense)
}) {
Text(text = "Save")
}
}
}
)
}

View File

@ -0,0 +1,12 @@
package com.hyperling.expensetracker
sealed interface ExpenseEvent {
object SaveExpense: ExpenseEvent
data class SetName(val name: String): ExpenseEvent
data class SetCost(val cost: String): ExpenseEvent
data class SetRate(val rate: Rate): ExpenseEvent
object ShowDialog: ExpenseEvent
object HideDialog: ExpenseEvent
data class SortExpenses(val sortType: SortType): ExpenseEvent
data class DeleteExpense(val expense: Expense): ExpenseEvent
}

View File

@ -0,0 +1,126 @@
package com.hyperling.expensetracker
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
fun getEnumStringResourceID(enumName: String): Int {
return (
when (enumName) {
SortType.NAME.toString() -> R.string.NAME
SortType.COST.toString() -> R.string.COST
SortType.RATE.toString() -> R.string.RATE
else -> 0
}
)
}
@Composable
fun ExpenseScreen(
state: ExpenseState,
onEvent: (ExpenseEvent) -> Unit
){
Scaffold (
floatingActionButton = {
FloatingActionButton(onClick = {
onEvent(ExpenseEvent.ShowDialog)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add Expense",
)
}
}
){ padding ->
if(state.isAddingExpense){
ExpenseDialogAdd(state, onEvent)
}
LazyColumn (
contentPadding = padding,
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
){
item {
Row (){
Text(text = "Sort ascending by:")
}
Row (
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically
){
SortType.values().forEach { sortType ->
Row (
modifier = Modifier
.clickable {
onEvent(ExpenseEvent.SortExpenses(sortType))
},
verticalAlignment = Alignment.CenterVertically
){
RadioButton(
selected = state.sortType == sortType,
onClick = {
onEvent(ExpenseEvent.SortExpenses(sortType))
}
)
Text(text = stringResource(getEnumStringResourceID(sortType.name)))
}
}
}
Row (){
Text(text = "Expenses:")
}
}
items(state.expenses) {expense ->
Row (
modifier = Modifier.fillMaxWidth()
) {
Column (
modifier = Modifier.weight(1f)
) {
Text (
text = expense.name,
fontSize = 20.sp
)
Text(
text = "${expense.cost} per ${expense.rate}",
fontSize = 12.sp
)
}
IconButton(onClick = {
onEvent(ExpenseEvent.DeleteExpense(expense))
}) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete Expense"
)
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
package com.hyperling.expensetracker
data class ExpenseState(
val expenses: List<Expense> = emptyList(),
val name: String = "",
val cost: String = "",
val rate: Rate = Rate.MONTHLY,
val isAddingExpense: Boolean = false,
val sortType: SortType = SortType.NAME,
)

View File

@ -0,0 +1,98 @@
package com.hyperling.expensetracker
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ExpenseViewModel (
private val dao: ExpenseDao,
): ViewModel() {
private val _sortType = MutableStateFlow(SortType.NAME)
private val _state = MutableStateFlow(ExpenseState())
private val _expenses = _sortType
.flatMapLatest { sortType ->
when(sortType) {
SortType.NAME -> dao.selectExpensesName()
SortType.COST -> dao.selectExpensesCost()
SortType.RATE -> dao.selectExpensesRate()
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val state = combine(_state, _sortType, _expenses) { state, sortType, expenses ->
state.copy(
expenses = expenses,
sortType = sortType
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ExpenseState())
fun onEvent(event: ExpenseEvent) {
when(event) {
is ExpenseEvent.DeleteExpense -> {
viewModelScope.launch {
dao.deleteExpense(event.expense)
}
}
ExpenseEvent.HideDialog -> {
_state.update { it.copy(
isAddingExpense = false
) }
}
ExpenseEvent.SaveExpense -> {
val name = state.value.name
val cost = state.value.cost
val rate = state.value.rate
if (name.isBlank()
|| cost.isBlank()
//|| rate.isBlank() # TBD, enable this once Rate is working.
) {
return
}
val expense = Expense(name, cost, rate)
viewModelScope.launch {
dao.upsertExpense(expense)
}
_state.update { it.copy(
isAddingExpense = false,
name = "",
cost = "",
rate = Rate.MONTHLY,
) }
}
is ExpenseEvent.SetName -> {
_state.update { it.copy(
name = event.name
) }
}
is ExpenseEvent.SetCost -> {
_state.update { it.copy(
cost = event.cost
) }
}
is ExpenseEvent.SetRate -> {
_state.update { it.copy(
rate = event.rate
) }
}
ExpenseEvent.ShowDialog -> {
_state.update { it.copy(
isAddingExpense = true
) }
}
is ExpenseEvent.SortExpenses -> {
_sortType.value = event.sortType
}
}
}
}

View File

@ -0,0 +1,140 @@
package com.hyperling.expensetracker
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import com.hyperling.expensetracker.ui.theme.ExpenseTrackerTheme
class MainActivityOLD : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ExpenseTrackerTheme {
Surface {
Main()
}
}
}
}
}
// This seems like a great tutorial for what I need to do!
// https://www.answertopia.com/jetpack-compose/a-jetpack-compose-room-database-and-repository-tutorial/
@Composable
fun Main() {
val context = LocalContext.current
var sumDaily by remember { mutableStateOf(0.0) }
var sumWeekly by remember { mutableStateOf(0.0) }
var sumMonthly by remember { mutableStateOf(0.0) }
var sumYearly by remember { mutableStateOf(0.0) }
val sums = mapOf(
"Daily" to sumDaily,
"Weekly" to sumWeekly,
"Monthly" to sumMonthly,
"Yearly" to sumYearly,
)
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Current Expense Summary"
)
//for ((name, value) in sums) {
sums.forEach { (name, value) ->
Row (
horizontalArrangement = Arrangement.End,
){
Text(text = name + ": ")
Text(text = value.toString())
}
}
// FORTESTING
Text (text = String.format("%.2f",sumDaily))
Text (text = String.format("%.2f",sumWeekly))
Text (text = String.format("%.2f",sumMonthly))
Text (text = String.format("%.2f",sumYearly))
Row (
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Button(onClick = {
val intent = Intent(context, NewExpenseActivity::class.java)
context.startActivity(intent)
}) {
Text(text = "Create New Expense")
}
}
Text(text = "Expenses")
// https://medium.com/@rowaido.game/implementing-the-room-library-with-jetpack-compose-590d13101fa7
/* TODO:
ForEach over all DB records, show it, and add its value to the summary.
*/
val expenseArray = listOf(
Expense("Test", 180.0, 'Y', "My notes."),
Expense("Test2", 20.0, 'M', "My notes, 2!"),
)
expenseArray.forEach { expense ->
when (expense.freq) {
'D' -> sumYearly += (expense.cost * 365.25)
'W' -> sumYearly += (expense.cost * (365.25 / 7))
'M' -> sumYearly += (expense.cost * 12)
'Y' -> sumYearly += (expense.cost)
}
sumDaily = sumYearly / 365.25
sumWeekly = sumYearly / (365.25 / 7)
sumMonthly = sumYearly / 12
}
// FORTESTING
Text (text = String.format("$ %.2f",sumDaily))
Text (text = String.format("$ %.2f",sumWeekly))
Text (text = String.format("$ %.2f",sumMonthly))
Text (text = String.format("$ %.2f",sumYearly))
}
}
@Preview(showBackground = true)
@Composable
fun MainPreview() {
ExpenseTrackerTheme {
Main()
}
}

View File

@ -1,140 +1,66 @@
package com.hyperling.expensetracker
import android.content.Context
import android.content.Intent
import com.hyperling.expensetracker.ui.theme.ExpenseTrackerTheme
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import com.hyperling.expensetracker.ui.theme.ExpenseTrackerTheme
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.room.Room
import androidx.compose.material3.Surface
class MainActivity : ComponentActivity() {
private val _db by lazy {
Room.databaseBuilder(
applicationContext,
ExpenseDatabase::class.java,
"expenses.db"
).build()
}
private val _viewModel by viewModels<ExpenseViewModel> (
factoryProducer = {
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ExpenseViewModel(_db.dao) as T
}
}
}
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ExpenseTrackerTheme {
Surface {
Main()
}
val state by _viewModel.state.collectAsState()
ExpenseScreen(
state = state,
onEvent = _viewModel::onEvent
)
}
}
}
}
// This seems like a great tutorial for what I need to do!
// https://www.answertopia.com/jetpack-compose/a-jetpack-compose-room-database-and-repository-tutorial/
@Composable
fun Main() {
val context = LocalContext.current
var sumDaily by remember { mutableStateOf(0.0) }
var sumWeekly by remember { mutableStateOf(0.0) }
var sumMonthly by remember { mutableStateOf(0.0) }
var sumYearly by remember { mutableStateOf(0.0) }
val sums = mapOf(
"Daily" to sumDaily,
"Weekly" to sumWeekly,
"Monthly" to sumMonthly,
"Yearly" to sumYearly,
)
Column (
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
text = "Current Expense Summary"
)
//for ((name, value) in sums) {
sums.forEach { (name, value) ->
Row (
horizontalArrangement = Arrangement.End,
){
Text(text = name + ": ")
Text(text = value.toString())
}
}
// FORTESTING
Text (text = String.format("%.2f",sumDaily))
Text (text = String.format("%.2f",sumWeekly))
Text (text = String.format("%.2f",sumMonthly))
Text (text = String.format("%.2f",sumYearly))
Row (
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Button(onClick = {
val intent = Intent(context, NewExpenseActivity::class.java)
context.startActivity(intent)
}) {
Text(text = "Create New Expense")
}
}
Text(text = "Expenses")
// https://medium.com/@rowaido.game/implementing-the-room-library-with-jetpack-compose-590d13101fa7
/* TODO:
ForEach over all DB records, show it, and add its value to the summary.
*/
val expenseArray = listOf(
Expense("Test", 180.0, 'Y', "My notes."),
Expense("Test2", 20.0, 'M', "My notes, 2!"),
)
expenseArray.forEach { expense ->
when (expense.freq) {
'D' -> sumYearly += (expense.cost * 365.25)
'W' -> sumYearly += (expense.cost * (365.25 / 7))
'M' -> sumYearly += (expense.cost * 12)
'Y' -> sumYearly += (expense.cost)
}
sumDaily = sumYearly / 365.25
sumWeekly = sumYearly / (365.25 / 7)
sumMonthly = sumYearly / 12
}
// FORTESTING
Text (text = String.format("$ %.2f",sumDaily))
Text (text = String.format("$ %.2f",sumWeekly))
Text (text = String.format("$ %.2f",sumMonthly))
Text (text = String.format("$ %.2f",sumYearly))
}
}
/*
@Preview(showBackground = true)
@Composable
fun MainPreview() {
ExpenseTrackerTheme {
Main()
}
}
}
*/

View File

@ -0,0 +1,8 @@
package com.hyperling.expensetracker
enum class Rate {
DAILY,
WEEKLY,
MONTHLY,
YEARLY,
}

View File

@ -0,0 +1,7 @@
package com.hyperling.expensetracker
enum class SortType {
NAME,
COST,
RATE,
}

View File

@ -1,3 +1,6 @@
<resources>
<string name="app_name">Recurring Expenses</string>
<string name="NAME">Name</string>
<string name="COST">Cost</string>
<string name="RATE">Rate</string>
</resources>