Add categories to transactions

This commit is contained in:
William Brawner 2018-09-16 19:57:47 -05:00
parent 9cd1428db8
commit 85a91d0af0
40 changed files with 885 additions and 97 deletions

17
.project Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>MyAllowance</name>
<comment>Project MyAllowance created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,2 @@
connection.project.dir=
eclipse.preferences.version=1

6
app/.classpath Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
app/.project Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View file

@ -0,0 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1

View file

@ -18,7 +18,7 @@ android {
}
defaultConfig {
applicationId "com.wbrawner.budget"
minSdkVersion 23
minSdkVersion 24
targetSdkVersion 27
versionCode 1
versionName "1.0"
@ -29,6 +29,11 @@ android {
buildConfigField "String", "ACRA_URL", "\"${acra.getProperty("url")}\""
buildConfigField "String", "ACRA_USER", "\"${acra.getProperty("user")}\""
buildConfigField "String", "ACRA_PASS", "\"${acra.getProperty("pass")}\""
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
release {
@ -36,6 +41,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}
dependencies {

View file

@ -0,0 +1,113 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "b68fd456557e5153b2fe6514a5193c07",
"entities": [
{
"tableName": "Transaction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `title` TEXT NOT NULL, `date` TEXT NOT NULL, `description` TEXT NOT NULL, `amount` REAL NOT NULL, `categoryId` INTEGER, `type` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "amount",
"columnName": "amount",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "categoryId",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Category",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `name` TEXT NOT NULL, `amount` REAL NOT NULL, `repeat` TEXT, `color` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "amount",
"columnName": "amount",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "repeat",
"columnName": "repeat",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b68fd456557e5153b2fe6514a5193c07\")"
]
}
}

View file

@ -20,7 +20,8 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity android:name="com.wbrawner.budget.addedittransaction.AddEditTransactionActivity" />
<activity android:name="com.wbrawner.budget.transactions.AddEditTransactionActivity" />
<activity android:name="com.wbrawner.budget.categories.AddEditCategoryActivity" />
</application>
</manifest>

View file

@ -3,9 +3,10 @@ package com.wbrawner.budget
import android.app.Application
import android.arch.persistence.room.Room
import android.content.Context
import com.wbrawner.budget.data.TransactionDao
import com.wbrawner.budget.data.TransactionsDatabase
import com.wbrawner.budget.BuildConfig
import com.wbrawner.budget.data.dao.TransactionDao
import com.wbrawner.budget.data.BudgetDatabase
import com.wbrawner.budget.data.dao.CategoryDao
import com.wbrawner.budget.data.migrations.MIGRATION_1_2
import org.acra.ACRA
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraHttpSender
@ -18,23 +19,28 @@ import org.acra.sender.HttpSender
basicAuthPassword = BuildConfig.ACRA_PASS,
httpMethod = HttpSender.Method.POST)
class AllowanceApplication: Application() {
lateinit var database: TransactionsDatabase
lateinit var database: BudgetDatabase
private set
lateinit var dao: TransactionDao
lateinit var transactionDao: TransactionDao
private set
lateinit var categoryDao: CategoryDao
private set
override fun onCreate() {
super.onCreate()
database = Room.databaseBuilder(applicationContext, TransactionsDatabase::class.java, "transactions")
database = Room.databaseBuilder(applicationContext, BudgetDatabase::class.java, "transactions")
.addMigrations(MIGRATION_1_2())
.build()
dao = database.dao()
transactionDao = database.transactionDao()
categoryDao = database.categoryDao()
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
ACRA.init(this)
if (!BuildConfig.DEBUG) ACRA.init(this)
}
}
}

View file

@ -0,0 +1,96 @@
package com.wbrawner.budget.categories
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.graphics.Color
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import com.wbrawner.budget.R
import com.wbrawner.budget.data.model.Category
import kotlinx.android.synthetic.main.activity_add_edit_category.*
class AddEditCategoryActivity : AppCompatActivity() {
lateinit var viewModel: CategoryViewModel
var id: Int? = null
var menu: Menu? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_edit_category)
setSupportActionBar(action_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewModel = ViewModelProviders.of(this).get(CategoryViewModel::class.java)
if (intent?.hasExtra(EXTRA_CATEGORY_ID) == false) {
setTitle(R.string.title_add_category)
return
}
viewModel.getCategory(intent!!.extras!!.getInt(EXTRA_CATEGORY_ID))
.observe(this, Observer<Category> { category ->
if (category == null) {
menu?.findItem(R.id.action_delete)?.isVisible = false
return@Observer
}
id = category.id
setTitle(R.string.title_edit_category)
menu?.findItem(R.id.action_delete)?.isVisible = true
edit_category_name.setText(category.name)
edit_category_amount.setText(String.format("%.02f", category.amount))
})
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_add_edit, menu)
if (id != null) {
menu?.findItem(R.id.action_delete)?.isVisible = true
}
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
android.R.id.home -> onBackPressed()
R.id.action_save -> {
if (!validateFields()) return true
viewModel.saveCategory(Category(
id = id,
name = edit_category_name.text.toString(),
amount = edit_category_amount.text.toString().toDouble(),
color = Color.parseColor("#FF0000"),
repeat = "never"
))
finish()
}
R.id.action_delete -> {
viewModel.deleteCategoryById(this@AddEditCategoryActivity.id!!)
finish()
}
}
return true
}
private fun validateFields(): Boolean {
var errors = false
if (edit_category_name.text.isEmpty()) {
edit_category_name.error = getString(R.string.required_field_name)
errors = true
}
if (edit_category_amount.text.isEmpty()) {
edit_category_amount.error = getString(R.string.required_field_amount)
errors = true
}
return !errors
}
companion object {
const val EXTRA_CATEGORY_ID = "EXTRA_CATEGORY_ID"
}
}

View file

@ -0,0 +1,64 @@
package com.wbrawner.budget.categories
import android.arch.lifecycle.LifecycleOwner
import android.arch.lifecycle.Observer
import android.content.Intent
import android.support.v4.content.ContextCompat.startActivity
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import com.wbrawner.budget.R
import com.wbrawner.budget.categories.AddEditCategoryActivity.Companion.EXTRA_CATEGORY_ID
import com.wbrawner.budget.data.model.Category
class CategoryAdapter(
private val lifecycleOwner: LifecycleOwner,
private val data: List<Category>,
private val viewModel: CategoryViewModel
) : RecyclerView.Adapter<CategoryAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_category, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val category = data[position]
holder.title?.text = category.name
holder.amount?.text = String.format("${'$'}%.02f", category.amount)
viewModel.getCurrentBalance(category.id!!)
.observe(lifecycleOwner, Observer<Double> { balance ->
holder.progress?.isIndeterminate = false
if (balance == null) {
holder.progress?.progress = 0
} else {
holder.progress?.max = category.amount.toInt()
holder.progress?.setProgress(
Math.abs(balance).toInt(),
true
)
}
})
holder.itemView.setOnClickListener {
startActivity(
it.context.applicationContext,
Intent(it.context.applicationContext, AddEditCategoryActivity::class.java)
.apply {
putExtra(EXTRA_CATEGORY_ID, category.id)
},
null
)
}
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView? = itemView.findViewById(R.id.category_title)
val amount: TextView? = itemView.findViewById(R.id.category_amount)
val progress: ProgressBar? = itemView.findViewById(R.id.category_progress)
}
}

View file

@ -0,0 +1,69 @@
package com.wbrawner.budget.categories
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.text.emoji.widget.EmojiTextView
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.wbrawner.budget.R
import com.wbrawner.budget.data.model.Category
class CategoryListFragment : Fragment() {
lateinit var viewModel: CategoryViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (activity == null) {
return
}
if (savedInstanceState != null) {
return
}
viewModel = ViewModelProviders.of(activity!!).get(CategoryViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_transaction_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.list_transactions)
val fab = view.findViewById<FloatingActionButton>(R.id.fab_add_transaction)
recyclerView.layoutManager = LinearLayoutManager(activity)
viewModel.getCategories()
.observe(this, Observer<List<Category>> { data ->
val noDataView = view.findViewById<EmojiTextView>(R.id.transaction_list_no_data)
if (data == null || data.isEmpty()) {
recyclerView.adapter = null
noDataView?.setText("No data FIX ME")
recyclerView?.visibility = View.GONE
noDataView?.visibility = View.VISIBLE
} else {
recyclerView.adapter = CategoryAdapter(this, data, viewModel)
recyclerView.visibility = View.VISIBLE
noDataView.visibility = View.GONE
}
})
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
if (dy > 0) fab.hide() else fab.show()
}
})
fab.setOnClickListener {
startActivity(Intent(activity, AddEditCategoryActivity::class.java))
}
return view
}
companion object {
const val TAG_FRAGMENT = "categories"
const val TITLE_FRAGMENT = R.string.title_categories
}
}

View file

@ -0,0 +1,26 @@
package com.wbrawner.budget.categories
import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import com.wbrawner.budget.AllowanceApplication
import com.wbrawner.budget.data.model.Category
import com.wbrawner.budget.data.CategoryRepository
class CategoryViewModel(application: Application): AndroidViewModel(application) {
private val categoryRepo = CategoryRepository((application as AllowanceApplication).categoryDao)
fun getCategory(id: Int): LiveData<Category> = categoryRepo.getCategory(id)
fun getCategories(): LiveData<List<Category>> = categoryRepo.getCategories()
fun saveCategory(category: Category) = categoryRepo.save(category)
fun deleteCategory(category: Category) = categoryRepo.delete(category)
fun deleteCategoryById(id: Int) = categoryRepo.deleteById(id)
fun getCurrentBalance(id: Int) = categoryRepo.getCurrentBalance(id)
fun getTransactions(id: Int) = categoryRepo.getTransactions(id)
}

View file

@ -0,0 +1,16 @@
package com.wbrawner.budget.data
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.TypeConverters
import com.wbrawner.budget.data.dao.CategoryDao
import com.wbrawner.budget.data.dao.TransactionDao
import com.wbrawner.budget.data.model.Category
import com.wbrawner.budget.data.model.Transaction
@Database(entities = [(Transaction::class), (Category::class)], version = 2)
@TypeConverters(DateTypeConverter::class, TransactionTypeTypeConverter::class)
abstract class BudgetDatabase: RoomDatabase() {
abstract fun transactionDao(): TransactionDao
abstract fun categoryDao(): CategoryDao
}

View file

@ -0,0 +1,44 @@
package com.wbrawner.budget.data
import android.arch.lifecycle.LiveData
import android.os.Handler
import android.os.HandlerThread
import com.wbrawner.budget.data.dao.CategoryDao
import com.wbrawner.budget.data.model.Category
import com.wbrawner.budget.data.model.Transaction
class CategoryRepository(private val dao: CategoryDao) {
private val handler: Handler
init {
val thread = HandlerThread("category")
thread.start()
handler = Handler(thread.looper)
}
fun getCategories(): LiveData<List<Category>> = dao.loadMultiple()
fun getCategory(id: Int): LiveData<Category> = dao.load(id)
fun save(category: Category) {
handler.post { dao.save(category) }
}
fun delete(category: Category) {
handler.post { dao.delete(category) }
}
fun deleteById(id: Int) {
handler.post { dao.deleteById(id) }
}
fun getCurrentBalance(id: Int): LiveData<Double> = dao.getBalanceForCategory(id)
fun getTransactions(id: Int): LiveData<List<Transaction>> = dao.getTransactions(id)
}

View file

@ -3,10 +3,13 @@ package com.wbrawner.budget.data
import android.arch.lifecycle.LiveData
import android.os.Handler
import android.os.HandlerThread
import com.wbrawner.budget.data.dao.TransactionDao
import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.model.TransactionCategory
import com.wbrawner.budget.data.model.TransactionWithCategory
class TransactionRepository(val dao: TransactionDao) {
val handler: Handler
val uiHandler: Handler = Handler()
class TransactionRepository(private val dao: TransactionDao) {
private val handler: Handler
init {
val thread = HandlerThread("transactions")
@ -14,36 +17,13 @@ class TransactionRepository(val dao: TransactionDao) {
handler = Handler(thread.looper)
}
fun getTransactionsByType(count: Int, type: TransactionType): LiveData<List<Transaction>> =
fun getTransactionsByType(count: Int, type: TransactionType): LiveData<List<TransactionWithCategory>> =
dao.loadMultipleByType(count, type)
fun getTransactions(count: Int): LiveData<List<Transaction>> = dao.loadMultiple(count)
fun getTransactions(count: Int): LiveData<List<TransactionWithCategory>> = dao.loadMultiple(count)
// fun getTransactions(count: Int): LiveData<List<Transaction>> {
// val data = MutableLiveData<List<Transaction>>()
//
// handler.post {
// val transactions = ArrayList<Transaction>()
// for (i in 0..count) {
// transactions.add(Transaction(
// i,
// "Transaction $i",
// Date(),
// "Spent some money on something",
// (Math.random() * 100).toFloat(),
// TransactionType.EXPENSE
// ))
// }
//
// uiHandler.post {
// data.value = transactions
// }
// }
//
// return data
// }
fun getTransaction(id: Int): LiveData<Transaction> = dao.load(id)
fun getTransaction(id: Int): LiveData<TransactionWithCategory> = dao.load(id)
fun save(transaction: Transaction) {
@ -62,4 +42,6 @@ class TransactionRepository(val dao: TransactionDao) {
fun getCurrentBalance(): LiveData<Double> = dao.getBalance()
fun getCategories(): LiveData<List<TransactionCategory>> = dao.loadCategories()
}

View file

@ -1,11 +0,0 @@
package com.wbrawner.budget.data
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.TypeConverters
@Database(entities = [(Transaction::class)], version = 1, exportSchema = false)
@TypeConverters(DateTypeConverter::class, TransactionTypeTypeConverter::class)
abstract class TransactionsDatabase: RoomDatabase() {
abstract fun dao(): TransactionDao
}

View file

@ -1,5 +1,6 @@
package com.wbrawner.budget.data
import android.arch.persistence.room.TypeConverter
import java.text.SimpleDateFormat
import java.util.*

View file

@ -0,0 +1,37 @@
package com.wbrawner.budget.data.dao
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.Dao
import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query
import com.wbrawner.budget.data.model.Category
import com.wbrawner.budget.data.model.Transaction
@Dao
interface CategoryDao {
@Insert(onConflict = REPLACE)
fun save(category: Category)
@Query("SELECT * FROM `Category` WHERE id = :id")
fun load(id: Int): LiveData<Category>
@Query("SELECT * FROM `Category`")
fun loadMultiple(): LiveData<List<Category>>
@Query("SELECT " +
"(SELECT TOTAL(amount) from `Transaction` WHERE type = 'INCOME' AND categoryId = :categoryId) " +
"- (SELECT TOTAL(amount) from `Transaction` WHERE type = 'EXPENSE' AND categoryId = :categoryId)")
fun getBalanceForCategory(categoryId: Int): LiveData<Double>
@Delete
fun delete(category: Category)
@Query("DELETE FROM `Category` WHERE id = :id")
fun deleteById(id: Int)
@Query("SELECT * FROM `Transaction` WHERE categoryId = :id")
fun getTransactions(id: Int): LiveData<List<Transaction>>
}

View file

@ -1,4 +1,4 @@
package com.wbrawner.budget.data
package com.wbrawner.budget.data.dao
import android.arch.lifecycle.LiveData
import android.arch.persistence.room.Dao
@ -6,6 +6,10 @@ import android.arch.persistence.room.Delete
import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query
import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.model.TransactionCategory
import com.wbrawner.budget.data.TransactionType
import com.wbrawner.budget.data.model.TransactionWithCategory
@Dao
interface TransactionDao {
@ -13,13 +17,13 @@ interface TransactionDao {
fun save(transaction: Transaction)
@Query("SELECT * FROM `Transaction` WHERE id = :id")
fun load(id: Int): LiveData<Transaction>
fun load(id: Int): LiveData<TransactionWithCategory>
@Query("SELECT * FROM `Transaction` LIMIT :count")
fun loadMultiple(count: Int): LiveData<List<Transaction>>
fun loadMultiple(count: Int): LiveData<List<TransactionWithCategory>>
@Query("SELECT * FROM `Transaction` WHERE type = :type LIMIT :count")
fun loadMultipleByType(count: Int, type: TransactionType): LiveData<List<Transaction>>
fun loadMultipleByType(count: Int, type: TransactionType): LiveData<List<TransactionWithCategory>>
@Query("SELECT (SELECT TOTAL(amount) from `Transaction` WHERE type = 'INCOME') - (SELECT TOTAL(amount) from `Transaction` WHERE type = 'EXPENSE')")
fun getBalance(): LiveData<Double>
@ -29,4 +33,7 @@ interface TransactionDao {
@Query("DELETE FROM `Transaction` WHERE id = :id")
fun deleteById(id: Int)
@Query("SELECT id,name from `Category`")
fun loadCategories(): LiveData<List<TransactionCategory>>
}

View file

@ -0,0 +1,13 @@
package com.wbrawner.budget.data.migrations
import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.migration.Migration
class MIGRATION_1_2: Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `Category` (`id` INTEGER, `name` TEXT, `amount` REAL, " +
"`repeat` TEXT, `color` INTEGER PRIMARY KEY (`id`))")
database.execSQL("ALTER TABLE `Transaction` ADD COLUMN categoryId")
}
}

View file

@ -0,0 +1,15 @@
package com.wbrawner.budget.data.model
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
import android.support.annotation.ColorInt
@Entity
class Category(
@PrimaryKey
val id: Int?,
val name: String,
val amount: Double,
val repeat: String?,
@ColorInt val color: Int
)

View file

@ -1,7 +1,8 @@
package com.wbrawner.budget.data
package com.wbrawner.budget.data.model
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
import com.wbrawner.budget.data.TransactionType
import java.util.*
@Entity
@ -12,5 +13,6 @@ class Transaction(
val date: Date,
val description: String,
val amount: Double,
val categoryId: Int?,
val type: TransactionType
)

View file

@ -0,0 +1,10 @@
package com.wbrawner.budget.data.model
class TransactionCategory(
val id: Int,
val name: String
) {
override fun toString(): String {
return name
}
}

View file

@ -0,0 +1,14 @@
package com.wbrawner.budget.data.model
import android.arch.persistence.room.Embedded
import android.arch.persistence.room.Relation
import com.wbrawner.budget.data.model.Category
import com.wbrawner.budget.data.model.Transaction
class TransactionWithCategory {
@Embedded
lateinit var transaction: Transaction
@Relation(parentColumn = "id", entityColumn = "id")
lateinit var categorySet: Set<Category>
}

View file

@ -20,12 +20,7 @@ class OverviewFragment : Fragment() {
return
}
if (savedInstanceState != null) {
return
}
viewModel = ViewModelProviders.of(activity!!).get(TransactionViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -64,4 +59,4 @@ class OverviewFragment : Fragment() {
const val TAG_FRAGMENT = "overview"
const val TITLE_FRAGMENT = R.string.app_name
}
}
}

View file

@ -1,4 +1,4 @@
package com.wbrawner.budget.addedittransaction
package com.wbrawner.budget.transactions
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
@ -6,10 +6,12 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
import com.wbrawner.budget.R
import com.wbrawner.budget.data.Transaction
import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.model.TransactionCategory
import com.wbrawner.budget.data.TransactionType
import com.wbrawner.budget.transactions.TransactionViewModel
import com.wbrawner.budget.data.model.TransactionWithCategory
import kotlinx.android.synthetic.main.activity_add_edit_transaction.*
import java.util.*
@ -26,6 +28,22 @@ class AddEditTransactionActivity : AppCompatActivity() {
setSupportActionBar(action_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewModel = ViewModelProviders.of(this).get(TransactionViewModel::class.java)
viewModel.getCategories()
.observe(this, Observer<List<TransactionCategory>> { categories ->
val adapter = ArrayAdapter<TransactionCategory>(
this@AddEditTransactionActivity,
android.R.layout.simple_list_item_1
)
adapter.add(TransactionCategory(0, getString(R.string.uncategorized)))
if (categories == null || categories.isEmpty()) {
return@Observer
}
adapter.addAll(categories)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
edit_transaction_category.adapter = adapter
})
if (intent?.hasExtra(EXTRA_TYPE) == true) {
type = TransactionType.valueOf(intent?.extras?.getString(EXTRA_TYPE, "EXPENSE")
?: "EXPENSE")
@ -37,11 +55,12 @@ class AddEditTransactionActivity : AppCompatActivity() {
}
viewModel.getTransaction(intent!!.extras!!.getInt(EXTRA_TRANSACTION_ID))
.observe(this, Observer<Transaction> { transaction ->
if (transaction == null) {
.observe(this, Observer<TransactionWithCategory> { transactionWithCategory ->
if (transactionWithCategory == null) {
menu?.findItem(R.id.action_delete)?.isVisible = false
return@Observer
}
val transaction = transactionWithCategory.transaction
id = transaction.id
type = transaction.type
setTitle(type.editTitle)
@ -55,6 +74,15 @@ class AddEditTransactionActivity : AppCompatActivity() {
val month = field.get(Calendar.MONTH)
val day = field.get(Calendar.DAY_OF_MONTH)
edit_transaction_date.updateDate(year, month, day)
if (transactionWithCategory.categorySet.isNotEmpty()) {
val category = transactionWithCategory.categorySet.first()
for (i in 0 until edit_transaction_category.adapter.count) {
if (category.id == (edit_transaction_category.adapter.getItem(i) as TransactionCategory).id) {
edit_transaction_category.setSelection(i)
break
}
}
}
})
}
@ -81,7 +109,8 @@ class AddEditTransactionActivity : AppCompatActivity() {
date = cal.time,
description = edit_transaction_description.text.toString(),
amount = edit_transaction_amount.text.toString().toDouble(),
type = type
type = type,
categoryId = (edit_transaction_category.selectedItem as TransactionCategory).id
))
finish()
}

View file

@ -8,13 +8,15 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.wbrawner.budget.R
import com.wbrawner.budget.addedittransaction.AddEditTransactionActivity
import com.wbrawner.budget.addedittransaction.AddEditTransactionActivity.Companion.EXTRA_TRANSACTION_ID
import com.wbrawner.budget.data.Transaction
import com.wbrawner.budget.transactions.AddEditTransactionActivity.Companion.EXTRA_TRANSACTION_ID
import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.model.TransactionWithCategory
import java.text.SimpleDateFormat
class TransactionAdapter(private val data: List<Transaction>)
: RecyclerView.Adapter<TransactionAdapter.ViewHolder>() {
class TransactionAdapter() : RecyclerView.Adapter<TransactionAdapter.ViewHolder>() {
private lateinit var data: List<Any>
private lateinit var listType: Class<Any>
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_transaction, parent, false)
@ -24,7 +26,9 @@ class TransactionAdapter(private val data: List<Transaction>)
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val transaction = data[position]
val transaction: Transaction =
if (listType == TransactionWithCategory::class.java) (data[position] as TransactionWithCategory).transaction
else data[position] as Transaction
holder.title.text = transaction.title
holder.date.text = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT).format(transaction.date)
holder.amount.text = String.format("${'$'}%.02f", transaction.amount)
@ -44,7 +48,16 @@ class TransactionAdapter(private val data: List<Transaction>)
}
}
inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
constructor(transactions: List<Any>) : this() {
if (transactions.isEmpty()) {
return
}
listType = transactions.first().javaClass
data = transactions
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title = itemView.findViewById<TextView>(R.id.transaction_title)
val date = itemView.findViewById<TextView>(R.id.transaction_date)
val amount = itemView.findViewById<TextView>(R.id.transaction_amount)

View file

@ -6,6 +6,7 @@ import android.support.text.emoji.EmojiCompat
import android.support.text.emoji.bundled.BundledEmojiCompatConfig
import android.support.v7.app.AppCompatActivity
import com.wbrawner.budget.R
import com.wbrawner.budget.categories.CategoryListFragment
import com.wbrawner.budget.data.TransactionType
import com.wbrawner.budget.overview.OverviewFragment
import kotlinx.android.synthetic.main.activity_transaction_list.*
@ -21,6 +22,7 @@ class TransactionListActivity : AppCompatActivity() {
when (item.itemId) {
R.id.action_expenses -> updateFragment(TransactionType.EXPENSE)
R.id.action_income -> updateFragment(TransactionType.INCOME)
R.id.action_categories -> updateFragment(CategoryListFragment.TAG_FRAGMENT, CategoryListFragment.TITLE_FRAGMENT)
else ->
updateFragment(OverviewFragment.TAG_FRAGMENT, OverviewFragment.TITLE_FRAGMENT)
}
@ -38,13 +40,17 @@ class TransactionListActivity : AppCompatActivity() {
var fragment = supportFragmentManager.findFragmentByTag(tag)
val ft = supportFragmentManager.beginTransaction()
if (fragment == null) {
fragment = if (tag == "overview") OverviewFragment()
else
TransactionListFragment().apply {
arguments = Bundle().apply {
putSerializable(TransactionListFragment.ARG_TYPE, TransactionType.valueOf(tag))
fragment = when (tag) {
OverviewFragment.TAG_FRAGMENT -> OverviewFragment()
CategoryListFragment.TAG_FRAGMENT -> CategoryListFragment()
else -> {
TransactionListFragment().apply {
arguments = Bundle().apply {
putSerializable(TransactionListFragment.ARG_TYPE, TransactionType.valueOf(tag))
}
}
}
}
ft.add(R.id.content_container, fragment, tag)
}
for (fmFragment in supportFragmentManager.fragments) {

View file

@ -13,10 +13,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.wbrawner.budget.R
import com.wbrawner.budget.addedittransaction.AddEditTransactionActivity
import com.wbrawner.budget.addedittransaction.AddEditTransactionActivity.Companion.EXTRA_TYPE
import com.wbrawner.budget.data.Transaction
import com.wbrawner.budget.transactions.AddEditTransactionActivity.Companion.EXTRA_TYPE
import com.wbrawner.budget.data.TransactionType
import com.wbrawner.budget.data.model.TransactionWithCategory
class TransactionListFragment : Fragment() {
lateinit var viewModel: TransactionViewModel
@ -43,7 +42,7 @@ class TransactionListFragment : Fragment() {
val fab = view.findViewById<FloatingActionButton>(R.id.fab_add_transaction)
recyclerView.layoutManager = LinearLayoutManager(activity)
viewModel.getTransactionsByType(20, type)
.observe(this, Observer<List<Transaction>> { data ->
.observe(this, Observer<List<TransactionWithCategory>> { data ->
val noDataView = view.findViewById<EmojiTextView>(R.id.transaction_list_no_data)
if (data == null || data.isEmpty()) {
recyclerView.adapter = null

View file

@ -4,22 +4,25 @@ import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import com.wbrawner.budget.AllowanceApplication
import com.wbrawner.budget.data.Transaction
import com.wbrawner.budget.data.TransactionRepository
import com.wbrawner.budget.data.TransactionType
import com.wbrawner.budget.data.*
import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.model.TransactionCategory
import com.wbrawner.budget.data.model.TransactionWithCategory
class TransactionViewModel(application: Application): AndroidViewModel(application) {
private val transactionRepo = TransactionRepository((application as AllowanceApplication).dao)
private val transactionRepo = TransactionRepository((application as AllowanceApplication).transactionDao)
fun getTransaction(id: Int): LiveData<Transaction> = transactionRepo.getTransaction(id)
fun getTransaction(id: Int): LiveData<TransactionWithCategory> = transactionRepo.getTransaction(id)
fun getTransactions(count: Int): LiveData<List<Transaction>> = transactionRepo.getTransactions(count)
fun getTransactions(count: Int): LiveData<List<TransactionWithCategory>> = transactionRepo.getTransactions(count)
fun getTransactionsByType(count: Int, type: TransactionType): LiveData<List<Transaction>>
fun getTransactionsByType(count: Int, type: TransactionType): LiveData<List<TransactionWithCategory>>
= transactionRepo.getTransactionsByType(count, type)
fun getCurrentBalance(): LiveData<Double> = transactionRepo.getCurrentBalance()
fun getCategories(): LiveData<List<TransactionCategory>> = transactionRepo.getCategories()
fun saveTransaction(transaction: Transaction) = transactionRepo.save(transaction)
fun deleteTransaction(transaction: Transaction) = transactionRepo.delete(transaction)

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2l-5.5,9h11z"/>
<path
android:fillColor="#FF000000"
android:pathData="M17.5,17.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0"/>
<path
android:fillColor="#FF000000"
android:pathData="M3,13.5h8v8H3z"/>
</vector>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/action_bar">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<android.support.design.widget.TextInputLayout
android:id="@+id/container_edit_category_name"
style="@style/AppTheme.EditText.Container"
android:hint="@string/prompt_category_name"
app:layout_constraintBottom_toTopOf="@+id/container_edit_category_amount"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.design.widget.TextInputEditText
android:id="@+id/edit_category_name"
style="@style/AppTheme.EditText"
android:inputType="textCapWords" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/container_edit_category_amount"
style="@style/AppTheme.EditText.Container"
android:hint="@string/prompt_category_amount"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/container_edit_category_name">
<android.support.design.widget.TextInputEditText
android:id="@+id/edit_category_amount"
style="@style/AppTheme.EditText"
android:inputType="numberDecimal" />
</android.support.design.widget.TextInputLayout>
<!--<TextView-->
<!--android:id="@+id/container_edit_transaction_date"-->
<!--style="@style/AppTheme.EditText.Hint"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_marginStart="4dp"-->
<!--android:text="@string/prompt_transaction_date"-->
<!--app:layout_constraintBottom_toTopOf="@+id/edit_transaction_date"-->
<!--app:layout_constraintEnd_toEndOf="parent"-->
<!--app:layout_constraintStart_toStartOf="parent"-->
<!--app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_amount" />-->
<!--<DatePicker-->
<!--android:id="@+id/edit_transaction_date"-->
<!--style="@style/AppTheme.DatePicker"-->
<!--app:layout_constraintBottom_toBottomOf="parent"-->
<!--app:layout_constraintEnd_toEndOf="parent"-->
<!--app:layout_constraintStart_toStartOf="parent"-->
<!--app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" />-->
</android.support.constraint.ConstraintLayout>
</ScrollView>
</android.support.constraint.ConstraintLayout>

View file

@ -88,10 +88,31 @@
<DatePicker
android:id="@+id/edit_transaction_date"
style="@style/AppTheme.DatePicker"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/container_edit_transaction_category"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" />
<TextView
android:id="@+id/container_edit_transaction_category"
style="@style/AppTheme.EditText.Hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="@string/prompt_transaction_category"
app:layout_constraintBottom_toTopOf="@+id/edit_transaction_category"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_transaction_date" />
<Spinner
android:id="@+id/edit_transaction_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_category" />
</android.support.constraint.ConstraintLayout>
</ScrollView>
</android.support.constraint.ConstraintLayout>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/category_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorTextPrimary"
android:textSize="18sp"
app:layout_constraintBottom_toTopOf="@id/barrier"
app:layout_constraintEnd_toStartOf="@+id/category_amount"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/category_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintBottom_toTopOf="@id/barrier"
app:layout_constraintStart_toEndOf="@+id/category_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="category_title,category_amount" />
<ProgressBar
android:id="@+id/category_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintEnd_toStartOf="@+id/barrier"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier" />
</android.support.constraint.ConstraintLayout>

View file

@ -12,4 +12,8 @@
android:id="@+id/action_income"
android:icon="@drawable/ic_attach_money_black_24dp"
android:title="@string/title_income" />
<item
android:id="@+id/action_categories"
android:icon="@drawable/ic_baseline_category_24px"
android:title="@string/title_categories" />
</menu>

View file

@ -19,4 +19,13 @@
<string name="title_edit_expense">Edit Expense</string>
<string name="income_no_data">&#x1F333;\nGet to work! Money doesn\'t grow on trees after all</string>
<string name="expenses_no_data">&#x1F914;\nAre you sure you haven\'t spent any money?</string>
<string name="title_categories">Categories</string>
<string name="title_edit_category">Edit Category</string>
<string name="prompt_category_amount">Amount</string>
<string name="prompt_category_name">Name</string>
<string name="title_add_category">Add Category</string>
<string name="uncategorized">Uncategorized</string>
<string name="prompt_transaction_category">Category</string>
<string name="required_field_name">Name is a required field</string>
<string name="required_field_amount">Amount is a required field</string>
</resources>

View file

@ -10,7 +10,7 @@
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.wbrawner.budget"
android:targetClass="com.wbrawner.budget.addedittransaction.AddEditTransactionActivity">
android:targetClass="com.wbrawner.budget.transactions.AddEditTransactionActivity">
<extra
android:name="EXTRA_TRANSACTION_TYPE"
android:value="EXPENSE" />
@ -25,7 +25,7 @@
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.wbrawner.budget"
android:targetClass="com.wbrawner.budget.addedittransaction.AddEditTransactionActivity">
android:targetClass="com.wbrawner.budget.transactions.AddEditTransactionActivity">
<extra
android:name="EXTRA_TRANSACTION_TYPE"
android:value="INCOME" />

View file

@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.41'
ext.kotlin_version = '1.3.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip