分类迁移到设置

增加时间分类
This commit is contained in:
yovinchen 2024-11-27 00:26:51 +08:00
parent 316c2648ae
commit 21b8f020f5
5 changed files with 347 additions and 8 deletions

View File

@ -28,21 +28,21 @@ abstract class BookkeepingDatabase : RoomDatabase() {
private var Instance: BookkeepingDatabase? = null private var Instance: BookkeepingDatabase? = null
private val MIGRATION_1_2 = object : Migration(1, 2) { private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
try { try {
Log.d(TAG, "Starting migration from version 1 to 2") Log.d(TAG, "Starting migration from version 1 to 2")
// 检查表是否存在 // 检查表是否存在
val cursor = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'") val cursor = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='categories'")
val tableExists = cursor.moveToFirst() val tableExists = cursor.moveToFirst()
cursor.close() cursor.close()
if (tableExists) { if (tableExists) {
// 如果表存在,执行迁移 // 如果表存在,执行迁移
Log.d(TAG, "Categories table exists, performing migration") Log.d(TAG, "Categories table exists, performing migration")
database.execSQL("ALTER TABLE categories RENAME TO categories_old") db.execSQL("ALTER TABLE categories RENAME TO categories_old")
database.execSQL(""" db.execSQL("""
CREATE TABLE categories ( CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -50,16 +50,16 @@ abstract class BookkeepingDatabase : RoomDatabase() {
) )
""") """)
database.execSQL(""" db.execSQL("""
INSERT INTO categories (name, type) INSERT INTO categories (name, type)
SELECT name, type FROM categories_old SELECT name, type FROM categories_old
""") """)
database.execSQL("DROP TABLE categories_old") db.execSQL("DROP TABLE categories_old")
} else { } else {
// 如果表不存在,直接创建新表 // 如果表不存在,直接创建新表
Log.d(TAG, "Categories table does not exist, creating new table") Log.d(TAG, "Categories table does not exist, creating new table")
database.execSQL(""" db.execSQL("""
CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
@ -69,7 +69,7 @@ abstract class BookkeepingDatabase : RoomDatabase() {
} }
// 确保 bookkeeping_records 表存在 // 确保 bookkeeping_records 表存在
database.execSQL(""" db.execSQL("""
CREATE TABLE IF NOT EXISTS bookkeeping_records ( CREATE TABLE IF NOT EXISTS bookkeeping_records (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,

View File

@ -0,0 +1,12 @@
package com.yovinchen.bookkeeping.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "members")
data class Member(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val description: String = "" // 可选的描述信息
)

View File

@ -0,0 +1,99 @@
package com.yovinchen.bookkeeping.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.Member
import com.yovinchen.bookkeeping.model.TransactionType
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecordCard(
record: BookkeepingRecord,
members: List<Member>,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = record.category,
style = MaterialTheme.typography.titleMedium
)
if (record.description.isNotEmpty()) {
Text(
text = record.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
.format(record.date),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Column(
horizontalAlignment = Alignment.End
) {
Text(
text = "${if (record.type == TransactionType.EXPENSE) "-" else "+"}¥${String.format("%.2f", record.amount)}",
style = MaterialTheme.typography.titleMedium,
color = if (record.type == TransactionType.EXPENSE)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary
)
}
}
if (members.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "Members",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = members.joinToString(", ") { it.name },
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}

View File

@ -0,0 +1,170 @@
package com.yovinchen.bookkeeping.ui.dialog
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.yovinchen.bookkeeping.model.Member
@Composable
fun MemberManagementDialog(
onDismiss: () -> Unit,
members: List<Member>,
onAddMember: (String, String) -> Unit,
onDeleteMember: (Member) -> Unit,
onUpdateMember: (Member, String, String) -> Unit
) {
var showAddDialog by remember { mutableStateOf(false) }
var selectedMember by remember { mutableStateOf<Member?>(null) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("成员管理") },
text = {
Column {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
items(members) { member ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = member.name,
style = MaterialTheme.typography.titleMedium
)
if (member.description.isNotEmpty()) {
Text(
text = member.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Row {
IconButton(onClick = { selectedMember = member }) {
Icon(Icons.Default.Edit, "编辑成员")
}
IconButton(onClick = { onDeleteMember(member) }) {
Icon(Icons.Default.Delete, "删除成员")
}
}
}
if (members.indexOf(member) < members.size - 1) {
HorizontalDivider()
}
}
}
Button(
onClick = { showAddDialog = true },
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) {
Icon(Icons.Default.Add, "添加成员")
Spacer(modifier = Modifier.width(8.dp))
Text("添加成员")
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("关闭")
}
}
)
// 添加成员对话框
if (showAddDialog) {
MemberEditDialog(
onDismiss = { showAddDialog = false },
onConfirm = { name, description ->
onAddMember(name, description)
showAddDialog = false
}
)
}
// 编辑成员对话框
selectedMember?.let { member ->
MemberEditDialog(
onDismiss = { selectedMember = null },
onConfirm = { name, description ->
onUpdateMember(member, name, description)
selectedMember = null
},
initialName = member.name,
initialDescription = member.description
)
}
}
@Composable
private fun MemberEditDialog(
onDismiss: () -> Unit,
onConfirm: (String, String) -> Unit,
initialName: String = "",
initialDescription: String = ""
) {
var name by remember { mutableStateOf(initialName) }
var description by remember { mutableStateOf(initialDescription) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(if (initialName.isEmpty()) "添加成员" else "编辑成员") },
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("成员名称") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("描述(可选)") },
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
TextButton(
onClick = {
if (name.isNotBlank()) {
onConfirm(name.trim(), description.trim())
}
},
enabled = name.isNotBlank()
) {
Text("确定")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("取消")
}
}
)
}

View File

@ -0,0 +1,58 @@
package com.yovinchen.bookkeeping.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.Category
import com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val database = BookkeepingDatabase.getDatabase(application)
private val dao = database.bookkeepingDao()
private val _selectedCategoryType = MutableStateFlow(TransactionType.EXPENSE)
val selectedCategoryType: StateFlow<TransactionType> = _selectedCategoryType.asStateFlow()
val categories: StateFlow<List<Category>> = _selectedCategoryType
.flatMapLatest { type ->
dao.getCategoriesByType(type)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun setSelectedCategoryType(type: TransactionType) {
_selectedCategoryType.value = type
}
fun addCategory(name: String, type: TransactionType) {
viewModelScope.launch {
val category = Category(name = name, type = type)
dao.insertCategory(category)
}
}
fun deleteCategory(category: Category) {
viewModelScope.launch {
dao.deleteCategory(category)
}
}
fun updateCategory(category: Category, newName: String) {
viewModelScope.launch {
val updatedCategory = category.copy(name = newName)
dao.updateCategory(updatedCategory)
// 更新所有使用该类别的记录
dao.updateRecordCategories(category.name, newName)
}
}
suspend fun isCategoryInUse(categoryName: String): Boolean {
return dao.isCategoryInUse(categoryName)
}
}