A:
好的,請繼續提供剩餘的screen檔案,謝謝
Q:
好的,我們繼續完成 ui/screens/ 套件的回顧。以下是剩餘的所有管理與功能畫面的最終版程式碼。
第廿三步:最終程式碼回顧 (UI Screens 篇 - 完結)
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/AccountManagementScreen.kt
<details>
<summary><strong>點此展開 AccountManagementScreen.kt 程式碼</strong></summary>
Kotlin
package com.yourcompany.cashflow.ui.screens
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.local.entity.Account
import com.yourcompany.cashflow.data.local.entity.AccountType
import com.yourcompany.cashflow.ui.viewmodel.AccountViewModel
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountManagementScreen(
viewModel: AccountViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val accounts by viewModel.allAccounts.collectAsStateWithLifecycle()
var showDialog by remember { mutableStateOf(false) }
var accountToEdit by remember { mutableStateOf<Account?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("管理帳戶") },
navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
accountToEdit = null
showDialog = true
}) {
Icon(Icons.Default.Add, "新增帳戶")
}
}
) { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
items(accounts) { account ->
ListItem(
headlineContent = { Text(account.name) },
supportingContent = { Text("類型: ${account.assetClass.name}, 餘額: ${"%,.0f".format(account.initialBalance)}") },
trailingContent = {
Row {
IconButton(onClick = {
accountToEdit = account
showDialog = true
}) { Icon(Icons.Default.Edit, "編輯") }
IconButton(onClick = { viewModel.deleteAccount(account) }) { Icon(Icons.Default.Delete, "刪除") }
}
}
)
Divider()
}
}
}
if (showDialog) {
AddEditAccountDialog(
account = accountToEdit,
onDismiss = { showDialog = false },
onConfirm = { name, type, assetClass, balance ->
if (accountToEdit == null) {
viewModel.addAccount(name, type, assetClass, balance)
} else {
viewModel.updateAccount(accountToEdit!!.copy(name = name, type = type, assetClass = assetClass, initialBalance = balance))
}
showDialog = false
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddEditAccountDialog(
account: Account?,
onDismiss: () -> Unit,
onConfirm: (String, AccountType, AssetClass, Double) -> Unit
) {
var name by remember { mutableStateOf(account?.name ?: "") }
var balance by remember { mutableStateOf(account?.initialBalance?.toString() ?: "") }
var selectedType by remember { mutableStateOf(account?.type ?: AccountType.BANK) }
var selectedAssetClass by remember { mutableStateOf(account?.assetClass ?: AssetClass.CASH) }
var typeMenuExpanded by remember { mutableStateOf(false) }
var assetClassMenuExpanded by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(if (account == null) "新增帳戶" else "編輯帳戶") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("帳戶名稱") })
OutlinedTextField(value = balance, onValueChange = { balance = it }, label = { Text("初始餘額") })
// Account Type Dropdown
ExposedDropdownMenuBox(expanded = typeMenuExpanded, onExpandedChange = { typeMenuExpanded = it }) {
OutlinedTextField(value = selectedType.name, onValueChange = {}, readOnly = true,
label = { Text("帳戶類型") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = typeMenuExpanded) },
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(expanded = typeMenuExpanded, onDismissRequest = { typeMenuExpanded = false }) {
AccountType.values().forEach { type ->
DropdownMenuItem(text = { Text(type.name) }, onClick = { selectedType = type; typeMenuExpanded = false })
}
}
}
// Asset Class Dropdown
ExposedDropdownMenuBox(expanded = assetClassMenuExpanded, onExpandedChange = { assetClassMenuExpanded = it }) {
OutlinedTextField(value = selectedAssetClass.name, onValueChange = {}, readOnly = true,
label = { Text("資產類別") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = assetClassMenuExpanded) },
modifier = Modifier.menuAnchor()
)
ExposedDropdownMenu(expanded = assetClassMenuExpanded, onDismissRequest = { assetClassMenuExpanded = false }) {
AssetClass.values().forEach { assetClass ->
DropdownMenuItem(text = { Text(assetClass.name) }, onClick = { selectedAssetClass = assetClass; assetClassMenuExpanded = false })
}
}
}
}
},
confirmButton = {
Button(onClick = {
val balanceDouble = balance.toDoubleOrNull()
if (name.isNotBlank() && balanceDouble != null) {
onConfirm(name, selectedType, selectedAssetClass, balanceDouble)
}
}) { Text("儲存") }
},
dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
)
}
</details>
檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/CategoryManagementScreen.kt
<details>
<summary><strong>點此展開 CategoryManagementScreen.kt 程式碼</strong></summary>
Kotlin
package com.yourcompany.cashflow.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
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.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.local.entity.MainCategory
import com.yourcompany.cashflow.data.local.entity.SubCategory
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.data.model.MainCategoryWithSubCategories
import com.yourcompany.cashflow.ui.components.CategoryDialog
import com.yourcompany.cashflow.ui.viewmodel.CategoryViewModel
// Sealed Class to manage dialog state
sealed class CategoryDialogState {
object Hidden : CategoryDialogState()
data class AddingMainCategory(val type: TransactionType) : CategoryDialogState()
data class EditingMainCategory(val mainCategory: MainCategory) : CategoryDialogState()
data class AddingSubCategory(val parentCategory: MainCategory) : CategoryDialogState()
data class EditingSubCategory(val subCategory: SubCategory) : CategoryDialogState()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryManagementScreen(
viewModel: CategoryViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val categories by viewModel.categories.collectAsStateWithLifecycle()
var dialogState by remember { mutableStateOf<CategoryDialogState>(CategoryDialogState.Hidden) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("管理分類") },
navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
)
},
floatingActionButton = {
FloatingActionButton(onClick = { dialogState = CategoryDialogState.AddingMainCategory(TransactionType.EXPENSE) }) {
Icon(Icons.Default.Add, "新增主分類")
}
}
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding),
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(categories) { categoryWithSubs ->
ExpandableMainCategoryItem(
categoryWithSubCategories = categoryWithSubs,
onAddSubCategory = { mainCategory -> dialogState = CategoryDialogState.AddingSubCategory(mainCategory) },
onEditMainCategory = { mainCategory -> dialogState = CategoryDialogState.EditingMainCategory(mainCategory) },
onDeleteMainCategory = { viewModel.deleteMainCategory(it) },
onEditSubCategory = { subCategory -> dialogState = CategoryDialogState.EditingSubCategory(subCategory) },
onDeleteSubCategory = { viewModel.deleteSubCategory(it) }
)
}
}
}
CategoryDialog(
state = dialogState,
onDismiss = { dialogState = CategoryDialogState.Hidden },
onConfirm = { name, type ->
when (val currentState = dialogState) {
is CategoryDialogState.AddingMainCategory -> viewModel.addMainCategory(name, type!!)
is CategoryDialogState.EditingMainCategory -> viewModel.updateMainCategory(currentState.mainCategory.copy(name = name, type = type!!))
is CategoryDialogState.AddingSubCategory -> viewModel.addSubCategory(name, currentState.parentCategory.id)
is CategoryDialogState.EditingSubCategory -> viewModel.updateSubCategory(currentState.subCategory.copy(name = name))
else -> {}
}
dialogState = CategoryDialogState.Hidden
}
)
}
@Composable
fun ExpandableMainCategoryItem(
categoryWithSubCategories: MainCategoryWithSubCategories,
onAddSubCategory: (MainCategory) -> Unit,
onEditMainCategory: (MainCategory) -> Unit,
onDeleteMainCategory: (MainCategory) -> Unit,
onEditSubCategory: (SubCategory) -> Unit,
onDeleteSubCategory: (SubCategory) -> Unit
) {
var isExpanded by remember { mutableStateOf(false) }
val mainCategory = categoryWithSubCategories.mainCategory
val subCategories = categoryWithSubCategories.subCategories
Column {
ListItem(
modifier = Modifier.clickable { isExpanded = !isExpanded },
headlineContent = { Text(mainCategory.name) },
leadingContent = { Icon(if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.Default.KeyboardArrowRight, "Expand/Collapse") },
trailingContent = {
Row {
IconButton(onClick = { onAddSubCategory(mainCategory) }) { Icon(Icons.Default.Add, "Add SubCategory") }
IconButton(onClick = { onEditMainCategory(mainCategory) }) { Icon(Icons.Default.Edit, "Edit MainCategory") }
IconButton(onClick = { onDeleteMainCategory(mainCategory) }) { Icon(Icons.Default.Delete, "Delete MainCategory") }
}
}
)
AnimatedVisibility(visible = isExpanded) {
Column(modifier = Modifier.padding(start = 24.dp)) {
subCategories.forEach { subCategory ->
ListItem(
headlineContent = { Text(subCategory.name) },
trailingContent = {
Row {
IconButton(onClick = { onEditSubCategory(subCategory) }) { Icon(Icons.Default.Edit, "Edit SubCategory", tint = MaterialTheme.colorScheme.secondary) }
IconButton(onClick = { onDeleteSubCategory(subCategory) }) { Icon(Icons.Default.Delete, "Delete SubCategory", tint = MaterialTheme.colorScheme.secondary) }
}
}
)
}
if (subCategories.isEmpty()) {
ListItem(headlineContent = { Text("沒有子分類", color = Color.Gray) })
}
}
}
}
}
</details>
(由於檔案數量和程式碼長度非常多,為了避免回應過於龐大,我將在下一個回應中繼續提供剩餘的 Screen 檔案。)