Compare commits
	
		
			8 Commits
		
	
	
		
			49eb36514d
			...
			7b1c0db0cc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7b1c0db0cc | |||
| e3a2625f68 | |||
| a4513394d1 | |||
| d5d525f65a | |||
| bf61d878cc | |||
| 6c95d33526 | |||
| d8ecd7b621 | |||
| 845715122f | 
| @@ -1,23 +1,13 @@ | |||||||
| package com.hyperling.expensetracker | package com.hyperling.expensetracker | ||||||
|  |  | ||||||
| /*class Expense { | import androidx.room.Entity | ||||||
|     var name: String = "" | import androidx.room.PrimaryKey | ||||||
|     var cost: Double = 0.0 |  | ||||||
|     var freq: Char = '*' |  | ||||||
|     var note: String = "" |  | ||||||
|  |  | ||||||
|     /*public Expense (val name: String, val cost: Double, val freq: Char, val note: String) { | @Entity | ||||||
|         this.name = name | data class Expense ( | ||||||
|         this.name = name |     val name: String, | ||||||
|         this.name = name |     val cost: String, | ||||||
|         this.name = name |     val rate: Enum<Rate>, | ||||||
|     }*/ |     @PrimaryKey(autoGenerate = true) | ||||||
| }*/ |     val ID: Int = 0, | ||||||
|  | ) | ||||||
| class Expense ( |  | ||||||
|     var name: String = "", |  | ||||||
|     var cost: Double = 0.0, |  | ||||||
|     var freq: Char = '*', |  | ||||||
|     var note: String = "", |  | ||||||
| ) { |  | ||||||
| } |  | ||||||
							
								
								
									
										30
									
								
								app/src/main/java/com/hyperling/expensetracker/ExpenseDao.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/src/main/java/com/hyperling/expensetracker/ExpenseDao.kt
									
									
									
									
									
										Normal 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>> | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
| @@ -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") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										126
									
								
								app/src/main/java/com/hyperling/expensetracker/ExpenseScreen.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								app/src/main/java/com/hyperling/expensetracker/ExpenseScreen.kt
									
									
									
									
									
										Normal 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" | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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, | ||||||
|  | ) | ||||||
| @@ -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 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,136 +1,61 @@ | |||||||
| package com.hyperling.expensetracker | package com.hyperling.expensetracker | ||||||
|  |  | ||||||
| import android.content.Context | import com.hyperling.expensetracker.ui.theme.ExpenseTrackerTheme | ||||||
| import android.content.Intent |  | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import androidx.activity.ComponentActivity | import androidx.activity.ComponentActivity | ||||||
| import androidx.activity.compose.setContent | import androidx.activity.compose.setContent | ||||||
| import androidx.activity.enableEdgeToEdge | import androidx.activity.enableEdgeToEdge | ||||||
| import androidx.compose.foundation.layout.Arrangement | import androidx.activity.viewModels | ||||||
| import androidx.compose.foundation.layout.Column |  | ||||||
| import androidx.compose.foundation.layout.Row |  | ||||||
| import androidx.compose.foundation.layout.fillMaxSize | import androidx.compose.foundation.layout.fillMaxSize | ||||||
| import androidx.compose.foundation.layout.fillMaxWidth |  | ||||||
| import androidx.compose.foundation.layout.padding | 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.Scaffold | ||||||
| import androidx.compose.material3.Surface | import androidx.compose.material3.Surface | ||||||
| import androidx.compose.material3.Text | import androidx.compose.material3.Text | ||||||
| import androidx.compose.runtime.Composable | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.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.setValue |  | ||||||
| import androidx.compose.ui.Alignment |  | ||||||
| import androidx.compose.ui.Modifier | import androidx.compose.ui.Modifier | ||||||
| import androidx.compose.ui.platform.LocalContext |  | ||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.core.content.ContextCompat | import androidx.lifecycle.ViewModel | ||||||
| import androidx.core.content.ContextCompat.startActivity | import androidx.lifecycle.ViewModelProvider | ||||||
| import com.hyperling.expensetracker.ui.theme.ExpenseTrackerTheme | import androidx.room.Room | ||||||
|  | import androidx.compose.material3.Surface | ||||||
|  |  | ||||||
| class MainActivity : ComponentActivity() { | 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?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|         enableEdgeToEdge() |         enableEdgeToEdge() | ||||||
|         setContent { |         setContent { | ||||||
|             ExpenseTrackerTheme { |             ExpenseTrackerTheme { | ||||||
|                 Surface { |                 val state by _viewModel.state.collectAsState() | ||||||
|                     Main() |                 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) | @Preview(showBackground = true) | ||||||
| @Composable | @Composable | ||||||
| fun MainPreview() { | fun MainPreview() { | ||||||
| @@ -138,3 +63,4 @@ fun MainPreview() { | |||||||
|         Main() |         Main() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | */ | ||||||
							
								
								
									
										8
									
								
								app/src/main/java/com/hyperling/expensetracker/Rate.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/src/main/java/com/hyperling/expensetracker/Rate.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | package com.hyperling.expensetracker | ||||||
|  |  | ||||||
|  | enum class Rate { | ||||||
|  |     DAILY, | ||||||
|  |     WEEKLY, | ||||||
|  |     MONTHLY, | ||||||
|  |     YEARLY, | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package com.hyperling.expensetracker | ||||||
|  |  | ||||||
|  | enum class SortType { | ||||||
|  |     NAME, | ||||||
|  |     COST, | ||||||
|  |     RATE, | ||||||
|  | } | ||||||
| @@ -1,3 +1,6 @@ | |||||||
| <resources> | <resources> | ||||||
|     <string name="app_name">Recurring Expenses</string> |     <string name="app_name">Recurring Expenses</string> | ||||||
|  |     <string name="NAME">Name</string> | ||||||
|  |     <string name="COST">Cost</string> | ||||||
|  |     <string name="RATE">Rate</string> | ||||||
| </resources> | </resources> | ||||||
		Reference in New Issue
	
	Block a user