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

View file

@ -3,9 +3,10 @@ package com.wbrawner.budget
import android.app.Application import android.app.Application
import android.arch.persistence.room.Room import android.arch.persistence.room.Room
import android.content.Context import android.content.Context
import com.wbrawner.budget.data.TransactionDao import com.wbrawner.budget.data.dao.TransactionDao
import com.wbrawner.budget.data.TransactionsDatabase import com.wbrawner.budget.data.BudgetDatabase
import com.wbrawner.budget.BuildConfig import com.wbrawner.budget.data.dao.CategoryDao
import com.wbrawner.budget.data.migrations.MIGRATION_1_2
import org.acra.ACRA import org.acra.ACRA
import org.acra.annotation.AcraCore import org.acra.annotation.AcraCore
import org.acra.annotation.AcraHttpSender import org.acra.annotation.AcraHttpSender
@ -18,23 +19,28 @@ import org.acra.sender.HttpSender
basicAuthPassword = BuildConfig.ACRA_PASS, basicAuthPassword = BuildConfig.ACRA_PASS,
httpMethod = HttpSender.Method.POST) httpMethod = HttpSender.Method.POST)
class AllowanceApplication: Application() { class AllowanceApplication: Application() {
lateinit var database: TransactionsDatabase lateinit var database: BudgetDatabase
private set private set
lateinit var dao: TransactionDao lateinit var transactionDao: TransactionDao
private set
lateinit var categoryDao: CategoryDao
private set private set
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
database = Room.databaseBuilder(applicationContext, TransactionsDatabase::class.java, "transactions") database = Room.databaseBuilder(applicationContext, BudgetDatabase::class.java, "transactions")
.addMigrations(MIGRATION_1_2())
.build() .build()
dao = database.dao() transactionDao = database.transactionDao()
categoryDao = database.categoryDao()
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) 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.arch.lifecycle.LiveData
import android.os.Handler import android.os.Handler
import android.os.HandlerThread 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) { class TransactionRepository(private val dao: TransactionDao) {
val handler: Handler private val handler: Handler
val uiHandler: Handler = Handler()
init { init {
val thread = HandlerThread("transactions") val thread = HandlerThread("transactions")
@ -14,36 +17,13 @@ class TransactionRepository(val dao: TransactionDao) {
handler = Handler(thread.looper) 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) 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) { fun save(transaction: Transaction) {
@ -62,4 +42,6 @@ class TransactionRepository(val dao: TransactionDao) {
fun getCurrentBalance(): LiveData<Double> = dao.getBalance() 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 package com.wbrawner.budget.data
import android.arch.persistence.room.TypeConverter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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.lifecycle.LiveData
import android.arch.persistence.room.Dao 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.Insert
import android.arch.persistence.room.OnConflictStrategy.REPLACE import android.arch.persistence.room.OnConflictStrategy.REPLACE
import android.arch.persistence.room.Query 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 @Dao
interface TransactionDao { interface TransactionDao {
@ -13,13 +17,13 @@ interface TransactionDao {
fun save(transaction: Transaction) fun save(transaction: Transaction)
@Query("SELECT * FROM `Transaction` WHERE id = :id") @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") @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") @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')") @Query("SELECT (SELECT TOTAL(amount) from `Transaction` WHERE type = 'INCOME') - (SELECT TOTAL(amount) from `Transaction` WHERE type = 'EXPENSE')")
fun getBalance(): LiveData<Double> fun getBalance(): LiveData<Double>
@ -29,4 +33,7 @@ interface TransactionDao {
@Query("DELETE FROM `Transaction` WHERE id = :id") @Query("DELETE FROM `Transaction` WHERE id = :id")
fun deleteById(id: Int) 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.Entity
import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.PrimaryKey
import com.wbrawner.budget.data.TransactionType
import java.util.* import java.util.*
@Entity @Entity
@ -12,5 +13,6 @@ class Transaction(
val date: Date, val date: Date,
val description: String, val description: String,
val amount: Double, val amount: Double,
val categoryId: Int?,
val type: TransactionType 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 return
} }
if (savedInstanceState != null) {
return
}
viewModel = ViewModelProviders.of(activity!!).get(TransactionViewModel::class.java) viewModel = ViewModelProviders.of(activity!!).get(TransactionViewModel::class.java)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

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.Observer
import android.arch.lifecycle.ViewModelProviders import android.arch.lifecycle.ViewModelProviders
@ -6,10 +6,12 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.ArrayAdapter
import com.wbrawner.budget.R 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.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 kotlinx.android.synthetic.main.activity_add_edit_transaction.*
import java.util.* import java.util.*
@ -26,6 +28,22 @@ class AddEditTransactionActivity : AppCompatActivity() {
setSupportActionBar(action_bar) setSupportActionBar(action_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
viewModel = ViewModelProviders.of(this).get(TransactionViewModel::class.java) 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) { if (intent?.hasExtra(EXTRA_TYPE) == true) {
type = TransactionType.valueOf(intent?.extras?.getString(EXTRA_TYPE, "EXPENSE") type = TransactionType.valueOf(intent?.extras?.getString(EXTRA_TYPE, "EXPENSE")
?: "EXPENSE") ?: "EXPENSE")
@ -37,11 +55,12 @@ class AddEditTransactionActivity : AppCompatActivity() {
} }
viewModel.getTransaction(intent!!.extras!!.getInt(EXTRA_TRANSACTION_ID)) viewModel.getTransaction(intent!!.extras!!.getInt(EXTRA_TRANSACTION_ID))
.observe(this, Observer<Transaction> { transaction -> .observe(this, Observer<TransactionWithCategory> { transactionWithCategory ->
if (transaction == null) { if (transactionWithCategory == null) {
menu?.findItem(R.id.action_delete)?.isVisible = false menu?.findItem(R.id.action_delete)?.isVisible = false
return@Observer return@Observer
} }
val transaction = transactionWithCategory.transaction
id = transaction.id id = transaction.id
type = transaction.type type = transaction.type
setTitle(type.editTitle) setTitle(type.editTitle)
@ -55,6 +74,15 @@ class AddEditTransactionActivity : AppCompatActivity() {
val month = field.get(Calendar.MONTH) val month = field.get(Calendar.MONTH)
val day = field.get(Calendar.DAY_OF_MONTH) val day = field.get(Calendar.DAY_OF_MONTH)
edit_transaction_date.updateDate(year, month, day) 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, date = cal.time,
description = edit_transaction_description.text.toString(), description = edit_transaction_description.text.toString(),
amount = edit_transaction_amount.text.toString().toDouble(), amount = edit_transaction_amount.text.toString().toDouble(),
type = type type = type,
categoryId = (edit_transaction_category.selectedItem as TransactionCategory).id
)) ))
finish() finish()
} }

View file

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

View file

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

View file

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

View file

@ -4,22 +4,25 @@ import android.app.Application
import android.arch.lifecycle.AndroidViewModel import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData import android.arch.lifecycle.LiveData
import com.wbrawner.budget.AllowanceApplication import com.wbrawner.budget.AllowanceApplication
import com.wbrawner.budget.data.Transaction import com.wbrawner.budget.data.*
import com.wbrawner.budget.data.TransactionRepository import com.wbrawner.budget.data.model.Transaction
import com.wbrawner.budget.data.TransactionType import com.wbrawner.budget.data.model.TransactionCategory
import com.wbrawner.budget.data.model.TransactionWithCategory
class TransactionViewModel(application: Application): AndroidViewModel(application) { 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) = transactionRepo.getTransactionsByType(count, type)
fun getCurrentBalance(): LiveData<Double> = transactionRepo.getCurrentBalance() fun getCurrentBalance(): LiveData<Double> = transactionRepo.getCurrentBalance()
fun getCategories(): LiveData<List<TransactionCategory>> = transactionRepo.getCategories()
fun saveTransaction(transaction: Transaction) = transactionRepo.save(transaction) fun saveTransaction(transaction: Transaction) = transactionRepo.save(transaction)
fun deleteTransaction(transaction: Transaction) = transactionRepo.delete(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 <DatePicker
android:id="@+id/edit_transaction_date" android:id="@+id/edit_transaction_date"
style="@style/AppTheme.DatePicker" 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/container_edit_transaction_date" /> 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> </android.support.constraint.ConstraintLayout>
</ScrollView> </ScrollView>
</android.support.constraint.ConstraintLayout> </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:id="@+id/action_income"
android:icon="@drawable/ic_attach_money_black_24dp" android:icon="@drawable/ic_attach_money_black_24dp"
android:title="@string/title_income" /> android:title="@string/title_income" />
<item
android:id="@+id/action_categories"
android:icon="@drawable/ic_baseline_category_24px"
android:title="@string/title_categories" />
</menu> </menu>

View file

@ -19,4 +19,13 @@
<string name="title_edit_expense">Edit Expense</string> <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="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="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> </resources>

View file

@ -10,7 +10,7 @@
<intent <intent
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:targetPackage="com.wbrawner.budget" android:targetPackage="com.wbrawner.budget"
android:targetClass="com.wbrawner.budget.addedittransaction.AddEditTransactionActivity"> android:targetClass="com.wbrawner.budget.transactions.AddEditTransactionActivity">
<extra <extra
android:name="EXTRA_TRANSACTION_TYPE" android:name="EXTRA_TRANSACTION_TYPE"
android:value="EXPENSE" /> android:value="EXPENSE" />
@ -25,7 +25,7 @@
<intent <intent
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:targetPackage="com.wbrawner.budget" android:targetPackage="com.wbrawner.budget"
android:targetClass="com.wbrawner.budget.addedittransaction.AddEditTransactionActivity"> android:targetClass="com.wbrawner.budget.transactions.AddEditTransactionActivity">
<extra <extra
android:name="EXTRA_TRANSACTION_TYPE" android:name="EXTRA_TRANSACTION_TYPE"
android:value="INCOME" /> 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. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.2.41' ext.kotlin_version = '1.3.0'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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