分类迁移到设置
增加时间分类
This commit is contained in:
parent
316c2648ae
commit
21b8f020f5
@ -28,21 +28,21 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
private var Instance: BookkeepingDatabase? = null
|
||||
|
||||
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
try {
|
||||
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()
|
||||
cursor.close()
|
||||
|
||||
if (tableExists) {
|
||||
// 如果表存在,执行迁移
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
@ -50,16 +50,16 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
)
|
||||
""")
|
||||
|
||||
database.execSQL("""
|
||||
db.execSQL("""
|
||||
INSERT INTO categories (name, type)
|
||||
SELECT name, type FROM categories_old
|
||||
""")
|
||||
|
||||
database.execSQL("DROP TABLE categories_old")
|
||||
db.execSQL("DROP TABLE categories_old")
|
||||
} else {
|
||||
// 如果表不存在,直接创建新表
|
||||
Log.d(TAG, "Categories table does not exist, creating new table")
|
||||
database.execSQL("""
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
@ -69,7 +69,7 @@ abstract class BookkeepingDatabase : RoomDatabase() {
|
||||
}
|
||||
|
||||
// 确保 bookkeeping_records 表存在
|
||||
database.execSQL("""
|
||||
db.execSQL("""
|
||||
CREATE TABLE IF NOT EXISTS bookkeeping_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
|
12
app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt
Normal file
12
app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt
Normal 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 = "" // 可选的描述信息
|
||||
)
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user