Vibe coding全紀錄-33

A:

 好的,請開始下一步,第十八步:建立專案詳情與進度追蹤頁面,謝謝 

Q:

好的,我們來進行階段 A 的最後一步,這將讓我們完整的專案管理功能閉環。

這一步的目標是建立一個「專案儀表板」,也就是專案的詳情頁面。使用者在這裡可以一目了然地看到專案的預算使用狀況、剩餘多少錢,並能檢視所有與此專案相關的交易紀錄。


第十八步:建立專案詳情與進度追蹤頁面

第一步:擴充數據層以取得專案交易

我們需要讓數據層能夠回答兩個問題:「這個專案總共花了多少錢?」和「這個專案底下有哪些交易?」。這兩個查詢應該放在 TransactionDao 中,因為它們操作的是 transactions 資料表。

A. 更新 TransactionDao.kt

路徑: app/src/main/java/com/yourcompany/cashflow/data/local/dao/TransactionDao.kt

Kotlin
// ...
@Dao
interface TransactionDao {
    // ... (現有方法)

    // vvv-- 加入以下兩個新方法 --vvv

    /**
     * 根據 project ID,計算該專案所有交易的總金額
     * 回傳的 Double? 表示如果沒有任何交易,結果可能為 null
     */
    @Query("SELECT SUM(amount) FROM transactions WHERE projectId = :projectId")
    fun getSpendingForProject(projectId: Long): Flow<Double?>

    /**
     * 根據 project ID,取得所有相關交易的詳細資料列表
     */
    @Transaction
    @Query("SELECT * FROM transactions WHERE projectId = :projectId ORDER BY transactionDate DESC")
    fun getTransactionsForProject(projectId: Long): Flow<List<TransactionWithDetails>>
}

B. 更新 TransactionRepository.kt

在 Repository 層公開這兩個新方法。

路徑: app/src/main/java/com/yourcompany/cashflow/data/repository/TransactionRepository.kt

Kotlin
// ...
interface TransactionRepository {
    // ... (現有方法)
    fun getSpendingForProject(projectId: Long): Flow<Double?>
    fun getTransactionsForProject(projectId: Long): Flow<List<TransactionWithDetails>>
}

class TransactionRepositoryImpl @Inject constructor(...) : TransactionRepository {
    // ... (現有實作)
    override fun getSpendingForProject(projectId: Long): Flow<Double?> = transactionDao.getSpendingForProject(projectId)
    override fun getTransactionsForProject(projectId: Long): Flow<List<TransactionWithDetails>> = transactionDao.getTransactionsForProject(projectId)
}

第二步:升級 ProjectViewModel 以載入詳情數據

我們需要讓 ProjectViewModel 在使用者選擇某個專案後,去取得該專案的總花費和交易列表。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/viewmodel/ProjectViewModel.kt

Kotlin
// ...
import com.yourcompany.cashflow.data.model.TransactionWithDetails
import com.yourcompany.cashflow.data.repository.TransactionRepository // 注入新的 Repository

@HiltViewModel
class ProjectViewModel @Inject constructor(
    private val projectRepository: ProjectRepository,
    private val transactionRepository: TransactionRepository // <-- 1. 注入 TransactionRepository
) : ViewModel() {

    // ... (現有的 projects, selectedProject 等)
    
    // vvv-- 加入以下兩個新的數據流 --vvv

    /**
     * 監聽 selectedProject 的變化,當它不為 null 時,
     * 就去查詢該專案的總花費。
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    val projectSpending: StateFlow<Double> = selectedProject.flatMapLatest { project ->
        project?.let {
            transactionRepository.getSpendingForProject(it.id)
                .map { spending -> spending ?: 0.0 } // 如果是 null (沒花費),就當作 0.0
        } ?: flowOf(0.0) // 如果沒有選擇專案,花費就是 0.0
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = 0.0
    )

    /**
     * 同樣地,監聽 selectedProject 的變化,取得其交易列表
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    val projectTransactions: StateFlow<List<TransactionWithDetails>> = selectedProject.flatMapLatest { project ->
        project?.let {
            transactionRepository.getTransactionsForProject(it.id)
        } ?: flowOf(emptyList())
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )
}

第三步:建立專案詳情畫面 (ProjectDetailScreen)

這個畫面是專案的儀表板。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/ProjectDetailScreen.kt (新檔案)

<details>

<summary>點此展開 ProjectDetailScreen.kt 的完整程式碼</summary>

Kotlin
package com.yourcompany.cashflow.ui.screens

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.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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 androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.local.entity.Project
import com.yourcompany.cashflow.data.local.entity.ProjectStatus
import com.yourcompany.cashflow.ui.components.TransactionItem
import com.yourcompany.cashflow.ui.viewmodel.ProjectViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProjectDetailScreen(
    projectId: Long,
    viewModel: ProjectViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit,
    onNavigateToEditProject: (Long) -> Unit
) {
    val project by viewModel.selectedProject.collectAsStateWithLifecycle()
    val spending by viewModel.projectSpending.collectAsStateWithLifecycle()
    val transactions by viewModel.projectTransactions.collectAsStateWithLifecycle()

    LaunchedEffect(key1 = projectId) {
        viewModel.loadProject(projectId)
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(project?.name ?: "專案詳情") },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } },
                actions = {
                    // 編輯按鈕
                    IconButton(onClick = { onNavigateToEditProject(projectId) }) {
                        Icon(Icons.Default.Edit, "編輯專案")
                    }
                    // 標記為完成按鈕
                    if (project?.status == ProjectStatus.ACTIVE) {
                        IconButton(onClick = {
                            project?.let { viewModel.updateProjectStatus(it, ProjectStatus.COMPLETED) }
                        }) {
                            Icon(Icons.Default.CheckCircle, "標記為完成")
                        }
                    }
                }
            )
        }
    ) { innerPadding ->
        project?.let { p ->
            LazyColumn(
                modifier = Modifier.padding(innerPadding),
                contentPadding = PaddingValues(16.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                // 項目一:進度總覽卡片
                item {
                    ProjectSummaryCard(project = p, totalSpending = spending)
                }

                // 項目二:關聯交易按鈕 (為下一步準備)
                item {
                    OutlinedButton(
                        onClick = { /* TODO: Step 19: Navigate to Batch Link Screen */ },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text("關聯現有交易")
                    }
                }

                // 項目三:交易明細列表
                item {
                    Text("相關交易明細", style = MaterialTheme.typography.titleMedium)
                }
                if (transactions.isEmpty()) {
                    item { Text("目前沒有任何相關交易。") }
                } else {
                    items(transactions) { transactionDetail ->
                        TransactionItem(transactionDetails = transactionDetail, onClick = {
                            // TODO: 可選功能,點擊後跳轉到交易編輯頁面
                        })
                    }
                }
            }
        } ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            CircularProgressIndicator() // 如果資料還在載入中,顯示讀取動畫
        }
    }
}


@Composable
fun ProjectSummaryCard(project: Project, totalSpending: Double) {
    val remaining = project.budget - totalSpending
    val progress = if (project.budget > 0) (totalSpending / project.budget).toFloat() else 0f
    val isOverBudget = remaining < 0

    Card(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Text(project.description ?: "沒有描述", style = MaterialTheme.typography.bodyMedium)
            Divider()
            // 進度條
            LinearProgressIndicator(
                progress = progress,
                modifier = Modifier.fillMaxWidth().height(8.dp),
                color = if (isOverBudget) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
            )
            // 數字總覽
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
                SummaryItem(label = "預算", amount = project.budget)
                SummaryItem(label = "已花費", amount = totalSpending)
                SummaryItem(label = "剩餘", amount = remaining, color = if (isOverBudget) MaterialTheme.colorScheme.error else Color.Unspecified)
            }
        }
    }
}

@Composable
fun SummaryItem(label: String, amount: Double, color: Color = Color.Unspecified) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text(label, style = MaterialTheme.typography.labelSmall)
        Text(
            text = "NT$ ${"%,.0f".format(amount)}",
            style = MaterialTheme.typography.bodyLarge,
            fontWeight = FontWeight.Bold,
            color = color
        )
    }
}

</details>

第四步:調整並整合導航流程

我們需要調整一下點擊專案列表項目後的行為,讓它先進入「詳情頁」,而不是直接進入「編輯頁」。

A. 調整 ProjectListScreen.kt 的導航

點擊列表項目時,應該傳遞 projectId 給詳情頁。

路徑: app/src/main/java/com/yourcompany/cashflow/ui/screens/ProjectListScreen.kt

Kotlin
@Composable
fun ProjectListScreen(
    ...
    // 這個回呼的語意改為導航到詳情或編輯頁
    onNavigateToProject: (Long) -> Unit
) {
    ...
    Scaffold(
        ...
        floatingActionButton = {
            FloatingActionButton(onClick = { onNavigateToProject(0L) }) { ... } // 傳入 0 代表新增
        }
    ) { innerPadding ->
        Column(...) {
            ...
            LazyColumn(...) {
                items(projects) { project ->
                    ListItem(
                        ...
                        // 點擊列表項目,傳入該專案的 ID
                        modifier = Modifier.clickable { onNavigateToProject(project.id) }
                    )
                    ...
                }
            }
        }
    }
}

B. 更新 AppNavigation.kt (或 MainScreen.kt 中的 AppNavHost)

加入新的詳情頁路由,並調整現有路由的跳轉邏輯。

Kotlin
// ...
object Routes {
    // ...
    const val PROJECT_DETAIL = "project_detail" // 新增詳情頁路由
}

// 在 NavHost 中...
@Composable
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
    NavHost(...) {
        // ...

        // 設定頁 -> 專案列表頁
        composable(Routes.SETTINGS) {
            SettingsScreen(
                ...
                onNavigateToProjectManagement = { navController.navigate(Routes.PROJECT_LIST) }
            )
        }

        // 專案列表頁 -> 新增頁 or 詳情頁
        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")
                }
            )
        }
        
        // 新增/編輯專案頁 (路徑保持不變)
        composable(
            route = "${Routes.ADD_EDIT_PROJECT}?projectId={projectId}",
            ...
        ) { ... }
    }
}

儀表板完成!

現在執行 App,整個專案管理的流程就非常順暢了:

  1. 從「設定」進入「管理專案」頁面。

  2. 點擊右下角 + 新增一個專案。

  3. 回到專案列表,點擊你剛剛建立的專案,就會進入我們這一步製作的「詳情儀表板」。

  4. 在詳情頁,你可以看到所有數據,並可以點擊右上角的「編輯」按鈕,跳轉到編輯頁面。

這個儀表板讓專案追蹤變得非常直覺。下一步,我們將處理階段 B 的第一個挑戰,也就是你在需求中特別提到的第十九步:實作「批次關聯交易」的進階功能。準備好了嗎?