Vibe coding全紀錄-34

A:

 好的,請開始階段 B 的第一個作業,第十九步:實作「批次關聯交易」的進階功能,謝謝 

Q:

好的,我們立刻開始執行階段 B 的第一個任務。

這一步將實作一個相當進階且實用的功能:讓使用者可以回溯過去,將已經存在的交易「批次」地關聯到某個專案中。這會大大增加「專案管理」功能的彈性。


第十九步:實作「批次關聯交易」功能

我們的計畫如下:

  1. 強化數據層:加入兩個關鍵方法,一個用來查詢所有「尚未歸屬」的交易,另一個用來執行「批次更新」。

  2. 升級 ProjectViewModel:讓它能夠提供待選交易的列表,並處理關聯操作。

  3. 建立 LinkTransactionsScreen:打造一個全新的介面,讓使用者可以在交易列表上進行勾選。

  4. 整合導航:從「專案詳情頁」連結到我們的新畫面。

第一步:強化數據層以支援批次更新

A. 更新 TransactionDao.kt

我們需要能夠查詢未歸屬的交易,以及一個能一次更新多筆交易的強大方法。

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

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

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

    /**
     * 查詢所有未被指派到任何專案的交易 (projectId IS NULL)
     */
    @Transaction
    @Query("SELECT * FROM transactions WHERE projectId IS NULL ORDER BY transactionDate DESC")
    fun getUnassignedTransactions(): Flow<List<TransactionWithDetails>>

    /**
     * 批次更新一組交易,將它們的 projectId 設定為指定值
     * 這是透過 `WHERE id IN (:transactionIds)` 語法來實現的
     */
    @Query("UPDATE transactions SET projectId = :projectId WHERE id IN (:transactionIds)")
    suspend fun linkTransactionsToProject(transactionIds: List<Long>, projectId: Long)
}

B. 更新 TransactionRepository.kt

在 Repository 層公開這兩個新功能。

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

Kotlin
// ...
interface TransactionRepository {
    // ... (現有方法)
    fun getUnassignedTransactions(): Flow<List<TransactionWithDetails>>
    suspend fun linkTransactionsToProject(transactionIds: List<Long>, projectId: Long)
}

class TransactionRepositoryImpl @Inject constructor(...) : TransactionRepository {
    // ... (現有實作)
    override fun getUnassignedTransactions(): Flow<List<TransactionWithDetails>> = transactionDao.getUnassignedTransactions()
    override suspend fun linkTransactionsToProject(transactionIds: List<Long>, projectId: Long) = transactionDao.linkTransactionsToProject(transactionIds, projectId)
}

第二步:升級 ProjectViewModel 以管理待選交易

我們繼續在 ProjectViewModel 中擴充邏輯,因為這個功能是從專案詳情頁發起的。

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

Kotlin
// ...
@HiltViewModel
class ProjectViewModel @Inject constructor(...) : ViewModel() {
    // ... (現有屬性與方法)
    
    // vvv-- 加入以下數據流與方法 --vvv

    /**
     * 提供給「批次關聯畫面」使用的、所有未歸屬的交易列表
     */
    val unassignedTransactions: StateFlow<List<TransactionWithDetails>> =
        transactionRepository.getUnassignedTransactions()
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = emptyList()
            )

    /**
     * 讓 UI 呼叫此方法,執行批次關聯操作
     */
    fun linkTransactions(transactionIds: List<Long>) {
        viewModelScope.launch {
            // 確保我們有關聯的目標專案
            val projectId = selectedProject.value?.id ?: return@launch
            if (transactionIds.isNotEmpty()) {
                transactionRepository.linkTransactionsToProject(transactionIds, projectId)
            }
        }
    }
}

第三步:建立批次關聯畫面 (LinkTransactionsScreen)

這個新畫面是本步驟的核心,它包含了一個帶有勾選框的列表。

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

<details>

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

Kotlin
package com.yourcompany.cashflow.ui.screens

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.material.icons.filled.Done
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yourcompany.cashflow.data.model.TransactionWithDetails
import com.yourcompany.cashflow.ui.viewmodel.ProjectViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LinkTransactionsScreen(
    viewModel: ProjectViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    val unassignedTransactions by viewModel.unassignedTransactions.collectAsStateWithLifecycle()
    val project by viewModel.selectedProject.collectAsStateWithLifecycle()

    // 使用 Set 來管理被勾選的交易 ID,效率高
    var selectedIds by remember { mutableStateOf<Set<Long>>(emptySet()) }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("關聯交易至 \"${project?.name ?: ""}\"", maxLines = 1, overflow = TextOverflow.Ellipsis) },
                navigationIcon = { IconButton(onClick = onNavigateBack) { Icon(Icons.Default.ArrowBack, "返回") } },
                actions = {
                    // 只有在至少選擇一筆交易時,儲存按鈕才能點擊
                    IconButton(
                        onClick = {
                            viewModel.linkTransactions(selectedIds.toList())
                            onNavigateBack()
                        },
                        enabled = selectedIds.isNotEmpty()
                    ) {
                        Icon(Icons.Default.Done, "儲存關聯")
                    }
                }
            )
        }
    ) { innerPadding ->
        LazyColumn(
            modifier = Modifier.padding(innerPadding),
            contentPadding = PaddingValues(vertical = 8.dp)
        ) {
            if (unassignedTransactions.isEmpty()) {
                item {
                    Box(modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) {
                        Text("沒有可關聯的交易")
                    }
                }
            } else {
                items(unassignedTransactions) { transactionDetail ->
                    SelectableTransactionRow(
                        transactionDetails = transactionDetail,
                        isSelected = transactionDetail.transaction.id in selectedIds,
                        onToggleSelection = {
                            selectedIds = if (it) {
                                selectedIds + transactionDetail.transaction.id
                            } else {
                                selectedIds - transactionDetail.transaction.id
                            }
                        }
                    )
                }
            }
        }
    }
}

// 帶有勾選框的交易列表項目
@Composable
fun SelectableTransactionRow(
    transactionDetails: TransactionWithDetails,
    isSelected: Boolean,
    onToggleSelection: (Boolean) -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onToggleSelection(!isSelected) }
            .padding(horizontal = 16.dp, vertical = 8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = isSelected,
            onCheckedChange = null // 點擊事件由 Row 處理
        )
        Spacer(modifier = Modifier.width(16.dp))
        // 這裡可以重用 TransactionItem,或者為了版面簡潔,自己客製化
        Column {
            Text(transactionDetails.subCategory.name, style = MaterialTheme.typography.bodyLarge)
            Text(
                "NT$ ${"%,.0f".format(transactionDetails.transaction.amount)}",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.primary
            )
        }
    }
}

</details>

第四步:整合導航流程

最後,我們從「專案詳情頁」啟用那個預留的按鈕。

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

加入新的路由。

Kotlin
// ...
object Routes {
    // ...
    const val LINK_TRANSACTIONS = "link_transactions"
}

// 在 NavHost 中...
@Composable
fun AppNavHost(...) {
    NavHost(...) {
        // ...
        
        // 從 ProjectDetailScreen 跳轉到 LinkTransactionsScreen
        composable(
            route = "${Routes.PROJECT_DETAIL}/{projectId}",
            ...
        ) { backStackEntry ->
            val projectId = backStackEntry.arguments?.getLong("projectId") ?: 0L
            ProjectDetailScreen(
                projectId = projectId,
                onNavigateBack = { navController.popBackStack() },
                onNavigateToEditProject = { id -> ... },
                // 新增導航到關聯頁面的回呼
                onNavigateToLinkTransactions = {
                    navController.navigate(Routes.LINK_TRANSACTIONS)
                }
            )
        }

        // 新增關聯交易畫面的路由
        composable(Routes.LINK_TRANSACTIONS) {
            // 這個畫面會自動使用 ProjectDetailScreen 載入的同一個 ProjectViewModel 實例
            LinkTransactionsScreen(
                onNavigateBack = { navController.popBackStack() }
            )
        }
    }
}

B. 更新 ProjectDetailScreen.kt

啟用「關聯現有交易」按鈕。

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

Kotlin
// ...
@Composable
fun ProjectDetailScreen(
    ...
    onNavigateToLinkTransactions: () -> Unit // 新增導航回呼
) {
    // ...
    Scaffold(...) {
        project?.let { p ->
            LazyColumn(...) {
                // ...
                item {
                    OutlinedButton(
                        onClick = onNavigateToLinkTransactions, // <-- 啟用按鈕
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text("關聯現有交易")
                    }
                }
                // ...
            }
        }
    }
}

進階功能完成!

現在,使用者擁有極大的彈性,即使在專案建立之後,也能輕鬆地將過去的交易納入追蹤。這讓專案管理功能變得非常完整和強大。

這個進階功能完成後,「專案管理」模組的核心部分就全部完成了。下一步,我們準備好回頭去重構第廿步:重構「統計圖表」,讓它也能理解並呈現我們剛剛建立的這些專愛的專案數據了嗎?