Vibe coding全紀錄-55

A:

要請幫忙一步一步詳細仔細的列出最終版的下列程式碼,謝謝

​ui/screens/*

​MainScreen.kt (App 主框架)

​HomeScreen.kt

​StatisticsScreen.kt

​BalanceSheetScreen.kt

​BudgetPlanningScreen.kt

​SettingsScreen.kt

​AccountManagementScreen.kt

​CategoryManagementScreen.kt

​ProjectListScreen.kt

​ProjectDetailScreen.kt

​AddEditProjectScreen.kt

​LinkTransactionsScreen.kt

​AddTransactionScreen.kt 

Q:

好的,我們來進行這次完整程式碼回顧的最後一個,也是內容最豐富的部分。

screens 套件是我們 App 的心臟地帶,它將所有後端邏輯 (ViewModel)、數據 (StateFlows) 和可重複使用的積木 (Components) 組合成一個個使用者可以實際看到並互動的功能頁面。

以下是 ui/screens/ 套件底下所有畫面 Composable 的最終版程式碼。


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

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.yourcompany.cashflow.ui.navigation.AppNavHost
import com.yourcompany.cashflow.ui.navigation.BottomNavItem

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
    val navController = rememberNavController()
    Scaffold(
        bottomBar = { BottomNavigationBar(navController = navController) }
    ) { innerPadding ->
        AppNavHost(
            navController = navController,
            modifier = Modifier.padding(innerPadding)
        )
    }
}

@Composable
fun BottomNavigationBar(navController: NavController) {
    val items = listOf(
        BottomNavItem.Transactions,
        BottomNavItem.BalanceSheet,
        BottomNavItem.Planning,
        BottomNavItem.Settings
    )
    NavigationBar {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route

        items.forEach { item ->
            NavigationBarItem(
                icon = { Icon(item.icon, contentDescription = item.title) },
                label = { Text(item.title) },
                selected = currentRoute == item.route,
                onClick = {
                    navController.navigate(item.route) {
                        navController.graph.startDestinationRoute?.let { route ->
                            popUpTo(route) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.screens

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
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.PieChart
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.ui.components.BudgetSummaryCard
import com.yourcompany.cashflow.ui.components.TransactionItem
import com.yourcompany.cashflow.ui.viewmodel.TransactionViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    viewModel: TransactionViewModel = hiltViewModel(),
    onNavigateToTransaction: (Long) -> Unit,
    onNavigateToStatistics: () -> Unit
) {
    val transactions by viewModel.allTransactions.collectAsStateWithLifecycle()
    val userBudget by viewModel.userBudget.collectAsStateWithLifecycle()
    val currentMonthExpenses by viewModel.currentMonthExpenses.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("交易明細") },
                actions = {
                    IconButton(onClick = onNavigateToStatistics) {
                        Icon(Icons.Default.PieChart, contentDescription = "統計")
                    }
                }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { onNavigateToTransaction(0L) }) {
                Icon(Icons.Default.Add, contentDescription = "新增交易")
            }
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(8.dp),
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            // 只在使用者設定過預算時,才顯示卡片
            if (userBudget.annualBudget > 0) {
                item {
                    Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
                        BudgetSummaryCard(
                            userBudget = userBudget,
                            currentMonthExpenses = currentMonthExpenses
                        )
                    }
                }
            }

            items(transactions) { transactionDetail ->
                Box(modifier = Modifier.padding(horizontal = 8.dp)) {
                    TransactionItem(
                        transactionDetails = transactionDetail,
                        onClick = { onNavigateToTransaction(transactionDetail.transaction.id) }
                    )
                }
            }
        }
    }
}

</details>


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

<details>

<summary><strong>點此展開 StatisticsScreen.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.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.model.CategorySum
import com.yourcompany.cashflow.data.model.SubCategorySum
import com.yourcompany.cashflow.ui.components.SegmentedButton
import com.yourcompany.cashflow.ui.components.TransactionTypeTabs
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewModel
import com.yourcompany.cashflow.ui.viewmodel.StatisticsViewMode
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 StatisticsScreen(
    viewModel: StatisticsViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    val viewMode by viewModel.viewMode.collectAsStateWithLifecycle()
    val transactionType by viewModel.transactionType.collectAsStateWithLifecycle()
    val mainChartData by viewModel.mainChartData.collectAsStateWithLifecycle()
    val selectedProject by viewModel.selectedProject.collectAsStateWithLifecycle()
    val drillDownLevel1Data by viewModel.drillDownLevel1Data.collectAsStateWithLifecycle()
    val selectedMainCategoryInProject by viewModel.selectedMainCategoryInProject.collectAsStateWithLifecycle()
    val drillDownLevel2Data by viewModel.drillDownLevel2Data.collectAsStateWithLifecycle()

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("財務統計") },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } }
            )
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            item {
                SegmentedButton(
                    options = listOf("依分類", "依專案"),
                    selectedIndex = viewMode.ordinal,
                    onSelectedIndex = { index ->
                        viewModel.setViewMode(if (index == 0) StatisticsViewMode.BY_CATEGORY else StatisticsViewMode.BY_PROJECT)
                    }
                )
            }

            item {
                MainChartCard(
                    title = if (viewMode == StatisticsViewMode.BY_CATEGORY) "主分類消費統計" else "專案消費統計",
                    data = mainChartData,
                    transactionType = transactionType,
                    selectedItem = if (viewMode == StatisticsViewMode.BY_PROJECT) selectedProject else null, // Only highlight in project mode
                    onTransactionTypeChange = { viewModel.setTransactionType(it) },
                    onItemClick = { viewModel.selectItem(it) }
                )
            }
            
            item {
                AnimatedVisibility(visible = drillDownLevel1Data.isNotEmpty() && selectedProject != null) {
                    DrillDownCard(
                        title = "『${selectedProject?.categoryName}』內部分類",
                        data = drillDownLevel1Data,
                        selectedItem = selectedMainCategoryInProject,
                        onItemClick = { viewModel.selectDrillDownLevel1Item(it) }
                    )
                }
            }

            item {
                AnimatedVisibility(visible = drillDownLevel2Data.isNotEmpty() && selectedMainCategoryInProject != null) {
                    SubCategoryAllocationSection(
                        title = "『${selectedMainCategoryInProject?.categoryName}』消費佔比",
                        data = drillDownLevel2Data
                    )
                }
            }
        }
    }
}


@Composable
fun MainChartCard(
    title: String,
    data: List<CategorySum>,
    transactionType: com.yourcompany.cashflow.data.local.entity.TransactionType,
    selectedItem: CategorySum?,
    onTransactionTypeChange: (com.yourcompany.cashflow.data.local.entity.TransactionType) -> Unit,
    onItemClick: (CategorySum) -> Unit
) {
    val chartProducer = remember { ChartEntryModelProducer() }
    val entries = data.mapIndexed { index, categorySum -> entryOf(index.toFloat(), categorySum.amount.toFloat()) }
    chartProducer.setEntries(entries)

    val bottomAxisFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, _ ->
        data.getOrNull(value.toInt())?.categoryName ?: ""
    }
    
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleLarge)
            TransactionTypeTabs(
                selectedType = transactionType,
                onTypeSelected = onTransactionTypeChange
            )
            Spacer(modifier = Modifier.height(16.dp))
            if (entries.isNotEmpty()) {
                Chart(
                    chart = columnChart(),
                    chartModelProducer = chartProducer,
                    startAxis = rememberStartAxis(),
                    bottomAxis = rememberBottomAxis(
                        valueFormatter = bottomAxisFormatter,
                        labelRotationDegrees = 45f
                    ),
                    modifier = Modifier.height(250.dp)
                )
                Divider(modifier = Modifier.padding(vertical = 8.dp))
                data.forEach { item ->
                    ListItem(
                        headlineText = { Text(item.categoryName) },
                        trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
                        modifier = Modifier.clickable { onItemClick(item) },
                        colors = if (item == selectedItem) {
                            ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.primaryContainer)
                        } else {
                            ListItemDefaults.colors()
                        }
                    )
                }
            } else {
                Box(modifier = Modifier.height(250.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
                    Text("本月尚無相關紀錄")
                }
            }
        }
    }
}

@Composable
fun DrillDownCard(
    title: String,
    data: List<CategorySum>,
    selectedItem: CategorySum?,
    onItemClick: (CategorySum) -> Unit
) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(8.dp))
            data.forEach { item ->
                ListItem(
                    headlineText = { Text(item.categoryName) },
                    trailingContent = { Text("NT$ ${"%,.0f".format(item.amount)}") },
                    modifier = Modifier.clickable { onItemClick(item) },
                    colors = if (item == selectedItem) {
                        ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
                    } else {
                        ListItemDefaults.colors()
                    }
                )
            }
        }
    }
}

@Composable
fun SubCategoryAllocationSection(title: String, data: List<SubCategorySum>) {
    val totalAmount = data.sumOf { it.amount }

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(title, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(16.dp))
            data.forEach { subCategory ->
                val percentage = (subCategory.amount / totalAmount * 100)
                Row(
                    modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(text = subCategory.subCategoryName, modifier = Modifier.weight(1f))
                    LinearProgressIndicator(
                        progress = { percentage.toFloat() / 100f },
                        modifier = Modifier.weight(2f).padding(horizontal = 8.dp)
                    )
                    Text(text = "%.1f%%".format(percentage), modifier = Modifier.weight(1f), textAlign = TextAlign.End)
                }
            }
        }
    }
}

</details>


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

Help net security:Thrive 事件回應和補救可協助組織遏制和消除威脅

Thrive Incident Response & Remediation 是一項針對企業的即時網絡安全響應服務,旨在協助企業應對和解決安全威脅。這項服務不僅有助於迅速控制和清除威脅,還提供工程支援,以重建和恢復關鍵系統。

以下是我對這項服務的幾點心得:

  1. 即時響應和清除:Thrive Incident Response & Remediation 提供了一個專門的事件響應人員,以應對安全事件並立即採取行動來恢復服務。這有助於減少業務中斷和相關成本。

  2. 預防性措施:該服務通過與客戶進行事前規劃,並運行自動威脅評估,主動應對網絡安全威脅。這有助於提前發現並處理潛在的威脅。

  3. 優先級管理:Thrive Incident Response & Remediation 服務允許用戶報告事件,並保證 Thrive SOC 在 15 分鐘內做出反應,開始進行威脅評估和範圍確定。

  4. 成本效益:考慮到全球數據泄露的平均成本在 2023 年達到 445 萬美元,Thrive Incident Response & Remediation 可以幫助企業降低應對安全威脅所需的成本。

總之,Thrive Incident Response & Remediation 是一項強大的安全服務,可以幫助企業有效應對和解決網絡安全威脅,保護業務運營的連續性和穩定性。

詳情請看:
Thrive Incident Response & Remediation helps organizations contain and remove threats
Posted in  on 10月 31, 2025 by Kevin |  

Vibe coding全紀錄-54

A:

要請幫忙一步一步詳細仔細的列出最終版的下列程式碼,謝謝

ui/navigation/*

​Routes.kt

​BottomNavItem.kt

​AppNavHost.kt

​ui/components/*

​BudgetSummaryCard.kt

​CategoryDialog.kt

​ParameterSlider.kt

​SegmentedButton.kt

​TransactionItem.kt

​TransactionTypeTabs.kt 

Q:

好的,我們繼續進行專案的完整程式碼回顧。

接下來的這部分是 App 的「骨架」與「積木」。navigation 套件定義了 App 的結構與畫面間的跳轉邏輯,是整個 UI 的骨架;components 套件則包含了所有我們精心打造、可在多個畫面重複使用的 UI 元件,是構成 App 視覺風格的積木。

以下是 ui/navigation/ui/components/ 兩個套件底下所有檔案的最終版程式碼。


第廿三步:最終程式碼回顧 (UI Navigation & Components 篇)

套件: ui/navigation/

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

/**
 * 將所有導航路線定義為常數,避免在程式碼中使用字串,方便管理與重構。
 */
object Routes {
    // Bottom Navigation Main Routes
    const val HOME = "home"
    const val BALANCE_SHEET = "balance_sheet"
    const val PLANNING = "planning"
    const val SETTINGS = "settings"

    // Sub-screens
    const val TRANSACTION_SCREEN = "transaction_screen" // For Add/Edit
    const val STATISTICS = "statistics"
    const val ACCOUNT_MANAGEMENT = "account_management"
    const val CATEGORY_MANAGEMENT = "category_management"
    const val PROJECT_LIST = "project_list"
    const val ADD_EDIT_PROJECT = "add_edit_project"
    const val PROJECT_DETAIL = "project_detail"
    const val LINK_TRANSACTIONS = "link_transactions"
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBalance
import androidx.compose.material.icons.filled.AutoGraph
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector

/**
 * 定義底部導覽列的每一個項目,包含其路線、圖示和標題。
 */
sealed class BottomNavItem(val route: String, val icon: ImageVector, val title: String) {
    object Transactions : BottomNavItem(Routes.HOME, Icons.Default.List, "明細")
    object BalanceSheet : BottomNavItem(Routes.BALANCE_SHEET, Icons.Default.AccountBalance, "資產")
    object Planning : BottomNavItem(Routes.PLANNING, Icons.Default.AutoGraph, "規劃")
    object Settings : BottomNavItem(Routes.SETTINGS, Icons.Default.Settings, "設定")
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.navigation

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.yourcompany.cashflow.ui.screens.*

/**
 * App 的導航主機,定義了所有畫面與其對應的路線。
 */
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(
        navController = navController,
        startDestination = BottomNavItem.Transactions.route,
        modifier = modifier
    ) {
        // --- Bottom Navigation Screens ---
        composable(BottomNavItem.Transactions.route) {
            HomeScreen(
                onNavigateToTransaction = { transactionId ->
                    navController.navigate("${Routes.TRANSACTION_SCREEN}?transactionId=$transactionId")
                },
                onNavigateToStatistics = { navController.navigate(Routes.STATISTICS) }
            )
        }
        composable(BottomNavItem.BalanceSheet.route) { BalanceSheetScreen() }
        composable(BottomNavItem.Planning.route) { BudgetPlanningScreen() }
        composable(BottomNavItem.Settings.route) {
            SettingsScreen(
                onNavigateToAccountManagement = { navController.navigate(Routes.ACCOUNT_MANAGEMENT) },
                onNavigateToCategoryManagement = { navController.navigate(Routes.CATEGORY_MANAGEMENT) },
                onNavigateToProjectManagement = { navController.navigate(Routes.PROJECT_LIST) }
            )
        }
        
        // --- Other Screens ---
        composable(Routes.STATISTICS) { StatisticsScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.ACCOUNT_MANAGEMENT) { AccountManagementScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.CATEGORY_MANAGEMENT) { CategoryManagementScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(Routes.PROJECT_LIST) {
            ProjectListScreen(
                onNavigateBack = { navController.popBackStack() },
                onNavigateToProject = { projectId ->
                    if (projectId == 0L) {
                        navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=0")
                    } else {
                        navController.navigate("${Routes.PROJECT_DETAIL}/$projectId")
                    }
                }
            )
        }
        composable(
            route = "${Routes.PROJECT_DETAIL}/{projectId}",
            arguments = listOf(navArgument("projectId") { type = NavType.LongType })
        ) { backStackEntry ->
            val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
            ProjectDetailScreen(
                projectId = projectId,
                onNavigateBack = { navController.popBackStack() },
                onNavigateToEditProject = { id -> navController.navigate("${Routes.ADD_EDIT_PROJECT}?projectId=$id") },
                onNavigateToLinkTransactions = { navController.navigate(Routes.LINK_TRANSACTIONS) }
            )
        }
        composable(
            route = "${Routes.ADD_EDIT_PROJECT}?projectId={projectId}",
            arguments = listOf(navArgument("projectId") { type = NavType.LongType; defaultValue = 0L })
        ) { backStackEntry ->
            val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
            AddEditProjectScreen(
                projectId = projectId,
                onNavigateBack = { navController.popBackStack() }
            )
        }
        composable(Routes.LINK_TRANSACTIONS) { LinkTransactionsScreen(onNavigateBack = { navController.popBackStack() }) }
        composable(
            route = "${Routes.TRANSACTION_SCREEN}?transactionId={transactionId}",
            arguments = listOf(navArgument("transactionId") { type = NavType.LongType; defaultValue = 0L })
        ) { backStackEntry ->
            val transactionId = backStackEntry.arguments?.getLong("transactionId") ?: 0L
            AddTransactionScreen(
                transactionId = transactionId,
                onNavigateBack = { navController.popBackStack() }
            )
        }
    }
}

</details>


套件: ui/components/

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.preferences.UserBudget

@Composable
fun BudgetSummaryCard(
    userBudget: UserBudget,
    currentMonthExpenses: Double
) {
    val remaining = userBudget.monthlyBudget - currentMonthExpenses
    val progress = if (userBudget.monthlyBudget > 0) (currentMonthExpenses / userBudget.monthlyBudget).toFloat() else 0f
    val isOverBudget = remaining < 0

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("本月預算追蹤", style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(8.dp))
            Row(modifier = Modifier.fillMaxWidth()) {
                Text("剩餘", modifier = Modifier.weight(1f))
                Text(
                    "NT$ ${"%,.0f".format(remaining)}",
                    color = if (isOverBudget) MaterialTheme.colorScheme.error else Color.Unspecified,
                    fontWeight = FontWeight.Bold
                )
            }
            Spacer(modifier = Modifier.height(4.dp))
            LinearProgressIndicator(
                progress = { progress.coerceIn(0f, 1f) }, // Use lambda for state-derived values
                modifier = Modifier.fillMaxWidth(),
                color = if (isOverBudget) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
            )
            Spacer(modifier = Modifier.height(4.dp))
            Row(modifier = Modifier.fillMaxWidth()) {
                Text("支出: ${"%,.0f".format(currentMonthExpenses)}", style = MaterialTheme.typography.bodySmall)
                Spacer(modifier = Modifier.weight(1f))
                Text("預算: ${"%,.0f".format(userBudget.monthlyBudget)}", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.unit.dp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.ui.screens.CategoryDialogState

@Composable
fun CategoryDialog(
    state: CategoryDialogState,
    onDismiss: () -> Unit,
    onConfirm: (name: String, type: TransactionType?) -> Unit
) {
    if (state is CategoryDialogState.Hidden) return

    var name by remember {
        mutableStateOf(
            when (state) {
                is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
                is CategoryDialogState.EditingSubCategory -> state.subCategory.name
                else -> ""
            }
        )
    }

    var transactionType by remember {
        mutableStateOf(
            when (state) {
                is CategoryDialogState.AddingMainCategory -> state.type
                is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
                else -> TransactionType.EXPENSE
            }
        )
    }
    
    // Ensure state resets when dialog reappears for a different purpose
    LaunchedEffect(state) {
        name = when (state) {
            is CategoryDialogState.EditingMainCategory -> state.mainCategory.name
            is CategoryDialogState.EditingSubCategory -> state.subCategory.name
            else -> ""
        }
        transactionType = when (state) {
            is CategoryDialogState.AddingMainCategory -> state.type
            is CategoryDialogState.EditingMainCategory -> state.mainCategory.type
            else -> TransactionType.EXPENSE
        }
    }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = {
            Text(
                when (state) {
                    is CategoryDialogState.AddingMainCategory -> "新增主分類"
                    is CategoryDialogState.EditingMainCategory -> "編輯主分類"
                    is CategoryDialogState.AddingSubCategory -> "新增子分類至 \"${state.parentCategory.name}\""
                    is CategoryDialogState.EditingSubCategory -> "編輯子分類"
                    is CategoryDialogState.Hidden -> ""
                }
            )
        },
        text = {
            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
                OutlinedTextField(
                    value = name,
                    onValueChange = { name = it },
                    label = { Text("名稱") }
                )
                if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
                    TransactionTypeTabs(
                        selectedType = transactionType,
                        onTypeSelected = { transactionType = it }
                    )
                }
            }
        },
        confirmButton = {
            Button(
                onClick = {
                    if (name.isNotBlank()) {
                        val typeToConfirm = if (state is CategoryDialogState.AddingMainCategory || state is CategoryDialogState.EditingMainCategory) {
                            transactionType
                        } else {
                            null
                        }
                        onConfirm(name, typeToConfirm)
                    }
                }
            ) { Text("儲存") }
        },
        dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } }
    )
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight

@Composable
fun ParameterSlider(
    label: String,
    value: Float,
    onValueChange: (Float) -> Unit,
    valueRange: ClosedFloatingPointRange<Float>,
    steps: Int,
    formatValue: (Float) -> String
) {
    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(label, style = MaterialTheme.typography.bodyLarge)
            Spacer(modifier = Modifier.weight(1f))
            Text(formatValue(value), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
        }
        Slider(
            value = value,
            onValueChange = onValueChange,
            valueRange = valueRange,
            steps = steps
        )
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SegmentedButton(
    modifier: Modifier = Modifier,
    options: List<String>,
    selectedIndex: Int,
    onSelectedIndex: (Int) -> Unit
) {
    SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
        options.forEachIndexed { index, label ->
            SegmentedButton(
                selected = index == selectedIndex,
                onClick = { onSelectedIndex(index) },
                shape = when (index) {
                    0 -> SegmentedButtonDefaults.itemShape(index = 0, count = options.size)
                    options.size - 1 -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
                    else -> SegmentedButtonDefaults.itemShape(index = index, count = options.size)
                }
            ) {
                Text(label)
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.dp
import androidx.compose.ui.unit.sp
import com.yourcompany.cashflow.data.local.entity.TransactionType
import com.yourcompany.cashflow.data.model.TransactionWithDetails
import java.text.SimpleDateFormat
import java.util.*

@Composable
fun TransactionItem(
    transactionDetails: TransactionWithDetails,
    onClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable(onClick = onClick),
        shape = RoundedCornerShape(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Row(
            modifier = Modifier
                .padding(12.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = transactionDetails.subCategory.name,
                    fontWeight = FontWeight.Bold,
                    fontSize = 16.sp
                )
                if (!transactionDetails.transaction.note.isNullOrEmpty()) {
                    Spacer(modifier = Modifier.height(4.dp))
                    Text(
                        text = transactionDetails.transaction.note,
                        style = MaterialTheme.typography.bodySmall,
                        color = Color.Gray
                    )
                }
            }
            Column(horizontalAlignment = Alignment.End) {
                val amountColor = if (transactionDetails.transaction.type == TransactionType.INCOME)
                                    Color(0xFF2E7D32) // Dark Green
                                  else
                                    MaterialTheme.colorScheme.error

                Text(
                    text = "%,.0f".format(transactionDetails.transaction.amount),
                    color = amountColor,
                    fontWeight = FontWeight.SemiBold,
                    fontSize = 18.sp
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = SimpleDateFormat("MM/dd", Locale.getDefault()).format(Date(transactionDetails.transaction.transactionDate)),
                    style = MaterialTheme.typography.bodySmall,
                    color = Color.Gray
                )
            }
        }
    }
}

</details>


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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.components

import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.yourcompany.cashflow.data.local.entity.TransactionType

@Composable
fun TransactionTypeTabs(
    selectedType: TransactionType,
    onTypeSelected: (TransactionType) -> Unit
) {
    val types = listOf("支出", "收入")
    val selectedIndex = if (selectedType == TransactionType.EXPENSE) 0 else 1

    TabRow(selectedTabIndex = selectedIndex) {
        types.forEachIndexed { index, title ->
            Tab(
                selected = selectedIndex == index,
                onClick = {
                    val type = if (index == 0) TransactionType.EXPENSE else TransactionType.INCOME
                    onTypeSelected(type)
                },
                text = { Text(text = title) }
            )
        }
    }
}

</details>


以上是我們專案 ui/navigation/ui/components/ 套件下所有檔案的最終版本。它們定義了 App 的脈絡和視覺基礎。