Vibe coding全紀錄-56

A:

 請繼續提供剩餘的Screen檔案,謝謝 

Q:

好的,我們繼續完成 ui/screens/ 套件的回顧。


第廿三步:最終程式碼回顧 (UI Screens 篇 - 續)

檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/BalanceSheetScreen.kt

<details>

<summary><strong>點此展開 BalanceSheetScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.background
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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.AssetClass
import com.yourcompany.cashflow.data.model.AssetAllocation
import com.yourcompany.cashflow.data.model.MonthlyTotal
import com.yourcompany.cashflow.ui.viewmodel.BalanceSheetViewModel
import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.entryOf

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BalanceSheetScreen(
    viewModel: BalanceSheetViewModel = hiltViewModel()
    // onNavigateToAccountHistory: (Long) -> Unit // For future use
) {
    val yearlyTrend by viewModel.yearlyAssetTrend.collectAsStateWithLifecycle()
    val assetAllocation by viewModel.assetAllocation.collectAsStateWithLifecycle()
    val accountsWithValue by viewModel.accountsWithLatestValue.collectAsStateWithLifecycle()
    val allAccounts by viewModel.allAccounts.collectAsStateWithLifecycle()

    var showUpdateDialog by remember { mutableStateOf(false) }

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("資產負債表") })
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { showUpdateDialog = true }) {
                Icon(Icons.Default.Add, "更新本月餘額")
            }
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            item { YearlyTrendChart(data = yearlyTrend) }
            item { AssetAllocationSection(data = assetAllocation) }
            item { Text("帳戶列表", style = MaterialTheme.typography.titleMedium) }
            items(accountsWithValue) { accountItem ->
                ListItem(
                    headlineContent = { Text(accountItem.account.name) },
                    supportingContent = { Text(accountItem.account.assetClass.name) },
                    trailingContent = {
                        Text(
                            text = "NT$ ${"%,.0f".format(accountItem.latestValue ?: 0.0)}",
                            fontWeight = FontWeight.SemiBold,
                            style = MaterialTheme.typography.bodyLarge
                        )
                    }
                )
                Divider()
            }
        }
    }

    if (showUpdateDialog) {
        UpdateBalancesDialog(
            accounts = allAccounts,
            onDismiss = { showUpdateDialog = false },
            onConfirm = { updates ->
                updates.forEach { (accountId, value) ->
                    viewModel.addOrUpdateSnapshot(accountId, value)
                }
                showUpdateDialog = false
            }
        )
    }
}


@Composable
fun YearlyTrendChart(data: List<MonthlyTotal>) {
    val chartProducer = remember { ChartEntryModelProducer() }
    val entries = data.mapIndexed { index, monthlyTotal -> entryOf(index.toFloat(), monthlyTotal.totalValue.toFloat()) }
    chartProducer.setEntries(entries)

    val bottomAxisFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, _ ->
        val month = data.getOrNull(value.toInt())?.yearMonth?.substring(5, 7) ?: ""
        "${month}月"
    }

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("年度資產趨勢", style = MaterialTheme.typography.titleLarge)
            Spacer(modifier = Modifier.height(16.dp))
            if (entries.isNotEmpty()) {
                Chart(
                    chart = columnChart(),
                    chartModelProducer = chartProducer,
                    startAxis = rememberStartAxis(),
                    bottomAxis = rememberBottomAxis(valueFormatter = bottomAxisFormatter),
                    modifier = Modifier.height(200.dp)
                )
            } else {
                Box(modifier = Modifier.height(200.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
                    Text("尚無資料可繪製趨勢圖")
                }
            }
        }
    }
}

@Composable
fun AssetAllocationSection(data: List<AssetAllocation>) {
    val totalAssets = data.sumOf { it.totalValue }
    
    val colorMap = mapOf(
        AssetClass.CASH to Color(0xFF66BB6A),
        AssetClass.STOCK to Color(0xFF42A5F5),
        AssetClass.BOND to Color(0xFFFFA726),
        AssetClass.OTHER to Color(0xFF8D6E63)
    )

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("資產配置比例", style = MaterialTheme.typography.titleLarge)
            Spacer(modifier = Modifier.height(16.dp))
            if (totalAssets > 0) {
                data.forEach { allocation ->
                    val percentage = (allocation.totalValue / totalAssets * 100)
                    Row(
                        modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        Box(modifier = Modifier.size(16.dp).background(colorMap[allocation.assetClass] ?: Color.Gray))
                        Spacer(modifier = Modifier.width(8.dp))
                        Text(text = allocation.assetClass.name, modifier = Modifier.weight(1f))
                        Text(text = "NT$ ${"%,.0f".format(allocation.totalValue)}", modifier = Modifier.weight(1.5f), textAlign = TextAlign.End)
                        Text(text = "%.1f%%".format(percentage), modifier = Modifier.weight(1f), textAlign = TextAlign.End)
                    }
                }
            } else {
                Box(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), contentAlignment = Alignment.Center) {
                    Text("尚無資產可進行配置分析")
                }
            }
        }
    }
}

@Composable
fun UpdateBalancesDialog(
    accounts: List<Account>,
    onDismiss: () -> Unit,
    onConfirm: (updates: Map<Long, Double>) -> Unit
) {
    var inputValues by remember { mutableStateOf<Map<Long, String>>(emptyMap()) }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("更新本月餘額") },
        text = {
            LazyColumn {
                items(accounts) { account ->
                    OutlinedTextField(
                        value = inputValues[account.id] ?: "",
                        onValueChange = { newValue ->
                            if (newValue.all { it.isDigit() }) {
                                inputValues = inputValues + (account.id to newValue)
                            }
                        },
                        label = { Text(account.name) },
                        modifier = Modifier.padding(vertical = 4.dp)
                    )
                }
            }
        },
        confirmButton = {
            Button(onClick = {
                val updates = inputValues.mapValues { it.value.toDoubleOrNull() }
                                         .filterValues { it != null }
                                         .mapValues { it.value!! }
                onConfirm(updates)
            }) { Text("儲存") }
        },
        dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
    )
}

</details>


檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/BudgetPlanningScreen.kt

<details>

<summary><strong>點此展開 BudgetPlanningScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.ParameterSlider
import com.yourcompany.cashflow.ui.viewmodel.BudgetViewModel
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BudgetPlanningScreen(
    viewModel: BudgetViewModel = hiltViewModel()
) {
    val plannedReturnRate by viewModel.plannedReturnRate.collectAsStateWithLifecycle()
    val inflationRate by viewModel.inflationRate.collectAsStateWithLifecycle()
    val remainingYears by viewModel.remainingYears.collectAsStateWithLifecycle()
    val annualBudget by viewModel.annualDisposableBudget.collectAsStateWithLifecycle()
    val netWorth by viewModel.latestNetWorth.collectAsStateWithLifecycle()

    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("年度預算規劃") })
        },
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            item {
                ResultCard(
                    annualBudget = annualBudget,
                    currentNetWorth = netWorth?.value ?: 0.0
                )
            }

            item {
                ParametersCard(
                    returnRate = plannedReturnRate,
                    onReturnRateChange = { viewModel.setPlannedReturnRate(it) },
                    inflationRate = inflationRate,
                    onInflationRateChange = { viewModel.setInflationRate(it) },
                    years = remainingYears.toFloat(),
                    onYearsChange = { viewModel.setRemainingYears(it.toInt()) }
                )
            }

            item {
                Button(
                    onClick = {
                        viewModel.saveBudget()
                        scope.launch {
                            snackbarHostState.showSnackbar("預算已成功儲存!")
                        }
                    },
                    modifier = Modifier.fillMaxWidth(),
                    enabled = annualBudget > 0
                ) {
                    Text("儲存為本年度預算")
                }
            }

            item {
                Text(
                    "提示:此計算基於 PMT 模型,結果僅為財務參考,實際市場狀況可能有所不同。",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        }
    }
}


@Composable
fun ResultCard(annualBudget: Double, currentNetWorth: Double) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            Text("年度可支配預算", style = MaterialTheme.typography.titleMedium)
            Text(
                text = "NT$ ${"%,.0f".format(annualBudget)}",
                style = MaterialTheme.typography.displayMedium,
                fontWeight = FontWeight.Bold,
                color = MaterialTheme.colorScheme.primary
            )
            Text(
                text = "每月平均 NT$ ${"%,.0f".format(annualBudget / 12)}",
                style = MaterialTheme.typography.bodyLarge
            )
            Divider(modifier = Modifier.padding(vertical = 8.dp))
            Text("計算基礎:當前總淨值 NT$ ${"%,.0f".format(currentNetWorth)}")
        }
    }
}

@Composable
fun ParametersCard(
    returnRate: Float, onReturnRateChange: (Float) -> Unit,
    inflationRate: Float, onInflationRateChange: (Float) -> Unit,
    years: Float, onYearsChange: (Float) -> Unit
) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
            Text("規劃參數", style = MaterialTheme.typography.titleLarge)
            ParameterSlider(
                label = "規劃報酬率", value = returnRate, onValueChange = onReturnRateChange,
                valueRange = 0f..0.15f, steps = 14, formatValue = { "%.1f %%".format(it * 100) }
            )
            ParameterSlider(
                label = "通貨膨脹率", value = inflationRate, onValueChange = onInflationRateChange,
                valueRange = 0f..0.05f, steps = 9, formatValue = { "%.1f %%".format(it * 100) }
            )
            ParameterSlider(
                label = "剩餘年數", value = years, onValueChange = onYearsChange,
                valueRange = 5f..50f, steps = 44, formatValue = { "%.0f 年".format(it) }
            )
        }
    }
}

</details>


檔案路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/SettingsScreen.kt

<details>

<summary><strong>點此展開 SettingsScreen.kt 程式碼</strong></summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
    onNavigateToAccountManagement: () -> Unit,
    onNavigateToCategoryManagement: () -> Unit,
    onNavigateToProjectManagement: () -> Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(title = { Text("設定") })
        }
    ) { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            ListItem(
                headlineContent = { Text("管理帳戶") },
                modifier = Modifier.clickable(onClick = onNavigateToAccountManagement)
            )
            Divider()
            ListItem(
                headlineContent = { Text("管理分類") },
                modifier = Modifier.clickable(onClick = onNavigateToCategoryManagement)
            )
            Divider()
            ListItem(
                headlineContent = { Text("管理專案") },
                modifier = Modifier.clickable(onClick = onNavigateToProjectManagement)
            )
            Divider()
        }
    }
}

</details>


(由於檔案數量和程式碼長度非常多,為了避免回應過於龐大,我將在下一個回應中繼續提供剩餘的 Screen 檔案。)