diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDatabase.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDatabase.kt index e84b134..4e91098 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDatabase.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDatabase.kt @@ -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, diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt new file mode 100644 index 0000000..e2359b5 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/Member.kt @@ -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 = "" // 可选的描述信息 +) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordCard.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordCard.kt new file mode 100644 index 0000000..46b4023 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/components/RecordCard.kt @@ -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, + 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 + ) + } + } + } + } +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/MemberManagementDialog.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/MemberManagementDialog.kt new file mode 100644 index 0000000..8edb7b1 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/dialog/MemberManagementDialog.kt @@ -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, + onAddMember: (String, String) -> Unit, + onDeleteMember: (Member) -> Unit, + onUpdateMember: (Member, String, String) -> Unit +) { + var showAddDialog by remember { mutableStateOf(false) } + var selectedMember by remember { mutableStateOf(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("取消") + } + } + ) +} diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/SettingsViewModel.kt new file mode 100644 index 0000000..354ac1c --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/SettingsViewModel.kt @@ -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 = _selectedCategoryType.asStateFlow() + + val categories: StateFlow> = _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) + } +}