From 3296f6d154a4069badd684b80c816421326b7c63 Mon Sep 17 00:00:00 2001 From: yovinchen Date: Thu, 5 Dec 2024 14:35:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E7=AD=9B=E9=80=89=E5=92=8C=E6=88=90=E5=91=98?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=98=BE=E7=A4=BA=201.=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20BookkeepingDao=20=E6=94=AF=E6=8C=81=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E7=AD=9B=E9=80=89=202.=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20CategoryDetailViewModel=20=E5=8F=8A=E5=85=B6=E5=B7=A5?= =?UTF-8?q?=E5=8E=82=E7=B1=BB=203.=20=E4=B8=BA=20MemberStat=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Room=20=E6=B3=A8=E8=A7=A3=204.=20=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=20CategoryDetailScreen=EF=BC=8C=E7=BB=93=E5=90=88=E9=A5=BC?= =?UTF-8?q?=E7=8A=B6=E5=9B=BE=E5=92=8C=E5=88=97=E8=A1=A8=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=205.=20=E4=BC=98=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=92=8C=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bookkeeping/data/BookkeepingDao.kt | 79 ++++++++++++--- .../yovinchen/bookkeeping/model/MemberStat.kt | 9 ++ .../ui/navigation/MainNavigation.kt | 99 ++++++++++++------- .../bookkeeping/ui/screen/AnalysisScreen.kt | 19 ++-- .../ui/screen/CategoryDetailScreen.kt | 75 +++++++++++--- .../ui/screen/MemberDetailScreen.kt | 13 ++- .../viewmodel/CategoryDetailViewModel.kt | 53 +++++----- .../CategoryDetailViewModelFactory.kt | 5 +- .../viewmodel/MemberDetailViewModel.kt | 85 ++++++++-------- 9 files changed, 303 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt index 3bcecb2..aea2425 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BookkeepingDao.kt @@ -3,7 +3,7 @@ package com.yovinchen.bookkeeping.data import androidx.room.* import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.Category -import com.yovinchen.bookkeeping.model.CategoryStat +import com.yovinchen.bookkeeping.model.MemberStat import com.yovinchen.bookkeeping.model.TransactionType import kotlinx.coroutines.flow.Flow import java.util.Date @@ -51,15 +51,11 @@ interface BookkeepingDao { ): Flow> @Query(""" - SELECT m.name as category, - SUM(r.amount) as amount, - COUNT(*) as count, - (SUM(r.amount) * 100.0 / ( - SELECT SUM(amount) - FROM bookkeeping_records - WHERE category = :category - AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth - )) as percentage + SELECT + m.name as member, + SUM(r.amount) as amount, + COUNT(*) as count, + (SUM(r.amount) * 100.0 / (SELECT SUM(amount) FROM bookkeeping_records WHERE category = :category AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth)) as percentage FROM bookkeeping_records r JOIN members m ON r.memberId = m.id WHERE r.category = :category @@ -70,7 +66,7 @@ interface BookkeepingDao { fun getMemberStatsByCategory( category: String, yearMonth: String - ): Flow> + ): Flow> @Query(""" SELECT * FROM bookkeeping_records @@ -81,6 +77,67 @@ interface BookkeepingDao { category: String ): Flow> + @Query(""" + SELECT * FROM bookkeeping_records + WHERE category = :category + AND date BETWEEN :startDate AND :endDate + ORDER BY date DESC + """) + fun getRecordsByCategoryAndDateRange( + category: String, + startDate: Date, + endDate: Date + ): Flow> + + @Query(""" + SELECT * FROM bookkeeping_records + WHERE memberId IN (SELECT id FROM members WHERE name = :memberName) + AND date BETWEEN :startDate AND :endDate + AND (:transactionType IS NULL OR type = :transactionType) + ORDER BY date DESC + """) + fun getRecordsByMemberAndDateRange( + memberName: String, + startDate: Date, + endDate: Date, + transactionType: TransactionType? + ): Flow> + + @Query(""" + SELECT * FROM bookkeeping_records + WHERE memberId IN (SELECT id FROM members WHERE name = :memberName) + AND category = :category + AND date BETWEEN :startDate AND :endDate + AND (:transactionType IS NULL OR type = :transactionType) + ORDER BY date DESC + """) + fun getRecordsByMemberCategoryAndDateRange( + memberName: String, + category: String, + startDate: Date, + endDate: Date, + transactionType: TransactionType? + ): Flow> + + @Query(""" + SELECT + m.name as member, + SUM(r.amount) as amount, + COUNT(*) as count, + (SUM(r.amount) * 100.0 / (SELECT SUM(amount) FROM bookkeeping_records WHERE category = :category AND date BETWEEN :startDate AND :endDate)) as percentage + FROM bookkeeping_records r + JOIN members m ON r.memberId = m.id + WHERE r.category = :category + AND r.date BETWEEN :startDate AND :endDate + GROUP BY m.name + ORDER BY amount DESC + """) + fun getMemberStatsByCategoryAndDateRange( + category: String, + startDate: Date, + endDate: Date + ): Flow> + @Insert suspend fun insertRecord(record: BookkeepingRecord): Long diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/MemberStat.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/MemberStat.kt index 75dce91..3c8be05 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/model/MemberStat.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/MemberStat.kt @@ -1,8 +1,17 @@ package com.yovinchen.bookkeeping.model +import androidx.room.ColumnInfo + data class MemberStat( + @ColumnInfo(name = "member") val member: String, + + @ColumnInfo(name = "amount") val amount: Double, + + @ColumnInfo(name = "count") val count: Int, + + @ColumnInfo(name = "percentage") val percentage: Double = 0.0 ) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt index 201bf25..aa7eb0f 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/navigation/MainNavigation.kt @@ -36,14 +36,30 @@ sealed class Screen( object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List) object Analysis : Screen("analysis", "分析", Icons.Default.Analytics) object Settings : Screen("settings", "设置", Icons.Default.Settings) - object CategoryDetail : Screen("category_detail/{category}/{yearMonth}", "分类详情") { - fun createRoute(category: String, yearMonth: YearMonth): String { - return "category_detail/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" + object CategoryDetail : Screen( + "category_detail/{category}/{startMonth}/{endMonth}", + "分类详情" + ) { + fun createRoute( + category: String, + startMonth: YearMonth, + endMonth: YearMonth + ): String { + return "category_detail/$category/${startMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}/${endMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" } } - object MemberDetail : Screen("member_detail/{memberName}/{category}/{yearMonth}?type={type}", "成员详情") { - fun createRoute(memberName: String, category: String, yearMonth: YearMonth, type: AnalysisType): String { - return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}" + object MemberDetail : Screen( + "member_detail/{memberName}/{category}/{startMonth}/{endMonth}?type={type}", + "成员详情" + ) { + fun createRoute( + memberName: String, + category: String, + startMonth: YearMonth, + endMonth: YearMonth, + type: AnalysisType + ): String { + return "member_detail/$memberName/$category/${startMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}/${endMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}" } } @@ -94,11 +110,11 @@ fun MainNavigation( composable(Screen.Analysis.route) { AnalysisScreen( - onNavigateToCategoryDetail = { category, yearMonth -> - navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth)) + onNavigateToCategoryDetail = { category, startMonth, endMonth -> + navController.navigate(Screen.CategoryDetail.createRoute(category, startMonth, endMonth)) }, - onNavigateToMemberDetail = { memberName, yearMonth, analysisType -> - navController.navigate(Screen.MemberDetail.createRoute(memberName, "", yearMonth, analysisType)) + onNavigateToMemberDetail = { memberName, startMonth, endMonth, analysisType -> + navController.navigate(Screen.MemberDetail.createRoute(memberName, "", startMonth, endMonth, analysisType)) } ) } @@ -114,51 +130,68 @@ fun MainNavigation( route = Screen.CategoryDetail.route, arguments = listOf( navArgument("category") { type = NavType.StringType }, - navArgument("yearMonth") { type = NavType.StringType } + navArgument("startMonth") { type = NavType.StringType }, + navArgument("endMonth") { type = NavType.StringType } ) ) { backStackEntry -> - val category = backStackEntry.arguments?.getString("category") ?: return@composable - val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable - val yearMonth = YearMonth.parse(yearMonthStr) - + val category = backStackEntry.arguments?.getString("category") ?: "" + val startMonth = YearMonth.parse( + backStackEntry.arguments?.getString("startMonth") ?: "", + DateTimeFormatter.ofPattern("yyyy-MM") + ) + val endMonth = YearMonth.parse( + backStackEntry.arguments?.getString("endMonth") ?: "", + DateTimeFormatter.ofPattern("yyyy-MM") + ) CategoryDetailScreen( category = category, - yearMonth = yearMonth, + startMonth = startMonth, + endMonth = endMonth, onNavigateBack = { navController.popBackStack() }, onNavigateToMemberDetail = { memberName -> - navController.navigate(Screen.MemberDetail.createRoute(memberName, category, yearMonth, AnalysisType.EXPENSE)) + navController.navigate( + Screen.MemberDetail.createRoute( + memberName = memberName, + category = category, + startMonth = startMonth, + endMonth = endMonth, + type = AnalysisType.EXPENSE + ) + ) } ) } - composable( route = Screen.MemberDetail.route, arguments = listOf( navArgument("memberName") { type = NavType.StringType }, navArgument("category") { type = NavType.StringType }, - navArgument("yearMonth") { type = NavType.StringType }, - navArgument("type") { + navArgument("startMonth") { type = NavType.StringType }, + navArgument("endMonth") { type = NavType.StringType }, + navArgument("type") { type = NavType.StringType defaultValue = AnalysisType.EXPENSE.name } ) ) { backStackEntry -> - val memberName = backStackEntry.arguments?.getString("memberName") ?: return@composable - val category = backStackEntry.arguments?.getString("category") ?: return@composable - val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable - val yearMonth = YearMonth.parse(yearMonthStr) - val type = backStackEntry.arguments?.getString("type")?.let { - try { - AnalysisType.valueOf(it) - } catch (e: IllegalArgumentException) { - AnalysisType.EXPENSE - } - } ?: AnalysisType.EXPENSE - + val memberName = backStackEntry.arguments?.getString("memberName") ?: "" + val category = backStackEntry.arguments?.getString("category") ?: "" + val startMonth = YearMonth.parse( + backStackEntry.arguments?.getString("startMonth") ?: "", + DateTimeFormatter.ofPattern("yyyy-MM") + ) + val endMonth = YearMonth.parse( + backStackEntry.arguments?.getString("endMonth") ?: "", + DateTimeFormatter.ofPattern("yyyy-MM") + ) + val type = AnalysisType.valueOf( + backStackEntry.arguments?.getString("type") ?: AnalysisType.EXPENSE.name + ) MemberDetailScreen( memberName = memberName, - yearMonth = yearMonth, category = category, + startMonth = startMonth, + endMonth = endMonth, analysisType = type, onNavigateBack = { navController.popBackStack() } ) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt index f3a2309..ebfb993 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/AnalysisScreen.kt @@ -48,8 +48,9 @@ enum class ViewMode { @OptIn(ExperimentalMaterial3Api::class) @Composable fun AnalysisScreen( - onNavigateToCategoryDetail: (String, YearMonth) -> Unit, - onNavigateToMemberDetail: (String, YearMonth, AnalysisType) -> Unit + onNavigateToCategoryDetail: (String, YearMonth, YearMonth) -> Unit, + onNavigateToMemberDetail: (String, YearMonth, YearMonth, AnalysisType) -> Unit, + modifier: Modifier = Modifier ) { val viewModel: AnalysisViewModel = viewModel() val startMonth by viewModel.startMonth.collectAsState() @@ -61,11 +62,13 @@ fun AnalysisScreen( var showViewModeMenu by remember { mutableStateOf(false) } var currentViewMode by rememberSaveable { mutableStateOf(ViewMode.CATEGORY) } - Scaffold { padding -> + Scaffold( + modifier = modifier.fillMaxSize() + ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() - .padding(padding) + .padding(paddingValues) ) { // 时间区间选择 DateRangePicker( @@ -151,9 +154,9 @@ fun AnalysisScreen( .padding(bottom = 16.dp), onCategoryClick = { category -> if (currentViewMode == ViewMode.CATEGORY) { - onNavigateToCategoryDetail(category, startMonth) + onNavigateToCategoryDetail(category, startMonth, endMonth) } else { - onNavigateToMemberDetail(category, startMonth, selectedAnalysisType) + onNavigateToMemberDetail(category, startMonth, endMonth, selectedAnalysisType) } } ) @@ -169,9 +172,9 @@ fun AnalysisScreen( stat = stat, onClick = { if (currentViewMode == ViewMode.CATEGORY && category != null) { - onNavigateToCategoryDetail(category, startMonth) + onNavigateToCategoryDetail(category, startMonth, endMonth) } else if (currentViewMode == ViewMode.MEMBER && member != null) { - onNavigateToMemberDetail(member, startMonth, selectedAnalysisType) + onNavigateToMemberDetail(member, startMonth, endMonth, selectedAnalysisType) } } ) diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt index 78b12ab..da17a00 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/CategoryDetailScreen.kt @@ -1,5 +1,6 @@ package com.yovinchen.bookkeeping.ui.screen +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -17,6 +18,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -33,6 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord +import com.yovinchen.bookkeeping.model.MemberStat import com.yovinchen.bookkeeping.model.TransactionType import com.yovinchen.bookkeeping.ui.components.CategoryPieChart import com.yovinchen.bookkeeping.ui.components.RecordItem @@ -47,17 +50,20 @@ import java.util.Locale @Composable fun CategoryDetailScreen( category: String, - yearMonth: YearMonth, + startMonth: YearMonth, + endMonth: YearMonth, onNavigateBack: () -> Unit, onNavigateToMemberDetail: (String) -> Unit, + viewModel: CategoryDetailViewModel = viewModel( + factory = CategoryDetailViewModelFactory( + database = BookkeepingDatabase.getDatabase(LocalContext.current), + category = category, + startMonth = startMonth, + endMonth = endMonth + ) + ), modifier: Modifier = Modifier ) { - val context = LocalContext.current - val database = remember { BookkeepingDatabase.getDatabase(context) } - val viewModel: CategoryDetailViewModel = viewModel( - factory = CategoryDetailViewModelFactory(database, category, yearMonth) - ) - val records by viewModel.records.collectAsState() val memberStats by viewModel.memberStats.collectAsState() val total by viewModel.total.collectAsState() @@ -107,16 +113,16 @@ fun CategoryDetailScreen( } } - // 第二部分:扇形图 + // 第二部分:成员统计 item { Card( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + .padding(16.dp) ) { Column( modifier = Modifier + .fillMaxWidth() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -125,19 +131,33 @@ fun CategoryDetailScreen( style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 16.dp) ) + + // 饼状图 CategoryPieChart( - categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) }, - memberData = emptyList(), - currentViewMode = false, + categoryData = emptyList(), + memberData = memberStats.map { Pair(it.member, it.percentage.toFloat()) }, + currentViewMode = true, modifier = Modifier .fillMaxWidth() .height(200.dp), onCategoryClick = { memberName -> - if (records.isNotEmpty() && records.first().type == TransactionType.EXPENSE) { - onNavigateToMemberDetail(memberName) - } + onNavigateToMemberDetail(memberName) } ) + + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(16.dp)) + + // 成员列表 + Column { + memberStats.forEach { stat -> + MemberStatItem( + stat = stat, + onClick = { onNavigateToMemberDetail(stat.member) } + ) + } + } } } } @@ -208,6 +228,29 @@ fun CategoryDetailScreen( } } +@Composable +private fun MemberStatItem( + stat: MemberStat, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + ListItem( + headlineContent = { Text(stat.member) }, + supportingContent = { + Text( + buildString { + append("金额: ¥%.2f".format(stat.amount)) + append(" | ") + append("次数: ${stat.count}") + append(" | ") + append("占比: %.1f%%".format(stat.percentage)) + } + ) + }, + modifier = modifier.clickable(onClick = onClick) + ) +} + @Composable private fun RecordItem( record: BookkeepingRecord, diff --git a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt index 17079a7..4d145f9 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/ui/screen/MemberDetailScreen.kt @@ -44,7 +44,8 @@ import java.util.Locale @Composable fun MemberDetailScreen( memberName: String, - yearMonth: YearMonth, + startMonth: YearMonth, + endMonth: YearMonth, category: String = "", analysisType: AnalysisType = AnalysisType.EXPENSE, onNavigateBack: () -> Unit, @@ -53,8 +54,14 @@ fun MemberDetailScreen( val records by viewModel.memberRecords.collectAsState(initial = emptyList()) val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0) - LaunchedEffect(memberName, category, yearMonth, analysisType) { - viewModel.loadMemberRecords(memberName, category, yearMonth, analysisType) + LaunchedEffect(memberName, category, startMonth, endMonth, analysisType) { + viewModel.loadMemberRecords( + memberName = memberName, + category = category, + startMonth = startMonth, + endMonth = endMonth, + analysisType = analysisType + ) } val groupedRecords = remember(records) { diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt index ada1d52..295f113 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModel.kt @@ -5,24 +5,25 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord -import com.yovinchen.bookkeeping.model.CategoryStat +import com.yovinchen.bookkeeping.model.MemberStat import kotlinx.coroutines.flow.* import java.time.YearMonth -import java.time.format.DateTimeFormatter +import java.time.ZoneId +import java.util.Date class CategoryDetailViewModel( private val database: BookkeepingDatabase, private val category: String, - private val month: YearMonth + private val startMonth: YearMonth, + private val endMonth: YearMonth ) : ViewModel() { private val recordDao = database.bookkeepingDao() - private val yearMonthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM")) private val _records = MutableStateFlow>(emptyList()) val records: StateFlow> = _records.asStateFlow() - private val _memberStats = MutableStateFlow>(emptyList()) - val memberStats: StateFlow> = _memberStats.asStateFlow() + private val _memberStats = MutableStateFlow>(emptyList()) + val memberStats: StateFlow> = _memberStats.asStateFlow() val total: StateFlow = records .map { records -> records.sumOf { it.amount } } @@ -33,22 +34,30 @@ class CategoryDetailViewModel( ) init { - recordDao.getRecordsByCategory(category) - .onEach { records -> - _records.value = records.filter { record -> - val recordMonth = YearMonth.from( - DateTimeFormatter.ofPattern("yyyy-MM") - .parse(yearMonthStr) - ) - YearMonth.from(record.date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) == recordMonth - } - } - .launchIn(viewModelScope) + val startDate = startMonth.atDay(1).atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } - recordDao.getMemberStatsByCategory(category, yearMonthStr) - .onEach { stats -> - _memberStats.value = stats - } - .launchIn(viewModelScope) + val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59) + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } + + recordDao.getRecordsByCategoryAndDateRange( + category = category, + startDate = startDate, + endDate = endDate + ) + .onEach { records -> _records.value = records } + .launchIn(viewModelScope) + + recordDao.getMemberStatsByCategoryAndDateRange( + category = category, + startDate = startDate, + endDate = endDate + ) + .onEach { stats -> _memberStats.value = stats } + .launchIn(viewModelScope) } } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt index 1a4e83d..61297a8 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/CategoryDetailViewModelFactory.kt @@ -8,12 +8,13 @@ import java.time.YearMonth class CategoryDetailViewModelFactory( private val database: BookkeepingDatabase, private val category: String, - private val month: YearMonth + private val startMonth: YearMonth, + private val endMonth: YearMonth ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) { @Suppress("UNCHECKED_CAST") - return CategoryDetailViewModel(database, category, month) as T + return CategoryDetailViewModel(database, category, startMonth, endMonth) as T } throw IllegalArgumentException("Unknown ViewModel class") } diff --git a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt index c623216..5cb290d 100644 --- a/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt +++ b/app/src/main/java/com/yovinchen/bookkeeping/viewmodel/MemberDetailViewModel.kt @@ -7,9 +7,7 @@ import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.AnalysisType import com.yovinchen.bookkeeping.model.TransactionType -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.* import java.time.YearMonth import java.time.ZoneId import java.util.Date @@ -19,47 +17,56 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica private val recordDao = database.bookkeepingDao() private val _memberRecords = MutableStateFlow>(emptyList()) - val memberRecords: StateFlow> = _memberRecords + val memberRecords: StateFlow> = _memberRecords.asStateFlow() private val _totalAmount = MutableStateFlow(0.0) - val totalAmount: StateFlow = _totalAmount + val totalAmount: StateFlow = _totalAmount.asStateFlow() - fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth, analysisType: AnalysisType) { - viewModelScope.launch { - val startDate = yearMonth.atDay(1).atStartOfDay() - .atZone(ZoneId.systemDefault()) - .toInstant() - .let { Date.from(it) } + fun loadMemberRecords( + memberName: String, + category: String, + startMonth: YearMonth, + endMonth: YearMonth, + analysisType: AnalysisType + ) { + val startDate = startMonth.atDay(1).atStartOfDay() + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } - val endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59) - .atZone(ZoneId.systemDefault()) - .toInstant() - .let { Date.from(it) } + val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59) + .atZone(ZoneId.systemDefault()) + .toInstant() + .let { Date.from(it) } - val transactionType = when (analysisType) { - AnalysisType.INCOME -> TransactionType.INCOME - AnalysisType.EXPENSE -> TransactionType.EXPENSE - else -> null - } - - val records = if (category.isEmpty()) { - recordDao.getRecordsByMember( - memberName = memberName, - startDate = startDate, - endDate = endDate, - transactionType = transactionType - ) - } else { - recordDao.getRecordsByMemberAndCategory( - memberName = memberName, - category = category, - startDate = startDate, - endDate = endDate, - transactionType = transactionType - ) - } - _memberRecords.value = records - _totalAmount.value = records.sumOf { it.amount } + val transactionType = when (analysisType) { + AnalysisType.INCOME -> TransactionType.INCOME + AnalysisType.EXPENSE -> TransactionType.EXPENSE + else -> null } + + val recordsFlow = if (category.isEmpty()) { + recordDao.getRecordsByMemberAndDateRange( + memberName = memberName, + startDate = startDate, + endDate = endDate, + transactionType = transactionType + ) + } else { + recordDao.getRecordsByMemberCategoryAndDateRange( + memberName = memberName, + category = category, + startDate = startDate, + endDate = endDate, + transactionType = transactionType + ) + } + + recordsFlow + .onEach { records -> + _memberRecords.value = records + _totalAmount.value = records.sumOf { it.amount } + } + .launchIn(viewModelScope) } }