feat: 增强时间范围筛选和成员统计显示

1. 更新 BookkeepingDao 支持时间范围筛选
2. 重构 CategoryDetailViewModel 及其工厂类
3. 为 MemberStat 添加 Room 注解
4. 改进 CategoryDetailScreen,结合饼状图和列表视图
5. 优化数据库查询和状态管理
This commit is contained in:
yovinchen 2024-12-05 14:35:01 +08:00
parent c92cc18dde
commit 3296f6d154
9 changed files with 303 additions and 134 deletions

View File

@ -3,7 +3,7 @@ package com.yovinchen.bookkeeping.data
import androidx.room.* import androidx.room.*
import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.Category 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 com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.Date import java.util.Date
@ -51,15 +51,11 @@ interface BookkeepingDao {
): Flow<List<BookkeepingRecord>> ): Flow<List<BookkeepingRecord>>
@Query(""" @Query("""
SELECT m.name as category, SELECT
SUM(r.amount) as amount, m.name as member,
COUNT(*) as count, SUM(r.amount) as amount,
(SUM(r.amount) * 100.0 / ( COUNT(*) as count,
SELECT SUM(amount) (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
WHERE category = :category
AND strftime('%Y-%m', datetime(date/1000, 'unixepoch')) = :yearMonth
)) as percentage
FROM bookkeeping_records r FROM bookkeeping_records r
JOIN members m ON r.memberId = m.id JOIN members m ON r.memberId = m.id
WHERE r.category = :category WHERE r.category = :category
@ -70,7 +66,7 @@ interface BookkeepingDao {
fun getMemberStatsByCategory( fun getMemberStatsByCategory(
category: String, category: String,
yearMonth: String yearMonth: String
): Flow<List<CategoryStat>> ): Flow<List<MemberStat>>
@Query(""" @Query("""
SELECT * FROM bookkeeping_records SELECT * FROM bookkeeping_records
@ -81,6 +77,67 @@ interface BookkeepingDao {
category: String category: String
): Flow<List<BookkeepingRecord>> ): Flow<List<BookkeepingRecord>>
@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<List<BookkeepingRecord>>
@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<List<BookkeepingRecord>>
@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<List<BookkeepingRecord>>
@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<List<MemberStat>>
@Insert @Insert
suspend fun insertRecord(record: BookkeepingRecord): Long suspend fun insertRecord(record: BookkeepingRecord): Long

View File

@ -1,8 +1,17 @@
package com.yovinchen.bookkeeping.model package com.yovinchen.bookkeeping.model
import androidx.room.ColumnInfo
data class MemberStat( data class MemberStat(
@ColumnInfo(name = "member")
val member: String, val member: String,
@ColumnInfo(name = "amount")
val amount: Double, val amount: Double,
@ColumnInfo(name = "count")
val count: Int, val count: Int,
@ColumnInfo(name = "percentage")
val percentage: Double = 0.0 val percentage: Double = 0.0
) )

View File

@ -36,14 +36,30 @@ sealed class Screen(
object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List) object Home : Screen("home", "记账", Icons.AutoMirrored.Filled.List)
object Analysis : Screen("analysis", "分析", Icons.Default.Analytics) object Analysis : Screen("analysis", "分析", Icons.Default.Analytics)
object Settings : Screen("settings", "设置", Icons.Default.Settings) object Settings : Screen("settings", "设置", Icons.Default.Settings)
object CategoryDetail : Screen("category_detail/{category}/{yearMonth}", "分类详情") { object CategoryDetail : Screen(
fun createRoute(category: String, yearMonth: YearMonth): String { "category_detail/{category}/{startMonth}/{endMonth}",
return "category_detail/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}" "分类详情"
) {
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}", "成员详情") { object MemberDetail : Screen(
fun createRoute(memberName: String, category: String, yearMonth: YearMonth, type: AnalysisType): String { "member_detail/{memberName}/{category}/{startMonth}/{endMonth}?type={type}",
return "member_detail/$memberName/$category/${yearMonth.format(DateTimeFormatter.ofPattern("yyyy-MM"))}?type=${type.name}" "成员详情"
) {
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) { composable(Screen.Analysis.route) {
AnalysisScreen( AnalysisScreen(
onNavigateToCategoryDetail = { category, yearMonth -> onNavigateToCategoryDetail = { category, startMonth, endMonth ->
navController.navigate(Screen.CategoryDetail.createRoute(category, yearMonth)) navController.navigate(Screen.CategoryDetail.createRoute(category, startMonth, endMonth))
}, },
onNavigateToMemberDetail = { memberName, yearMonth, analysisType -> onNavigateToMemberDetail = { memberName, startMonth, endMonth, analysisType ->
navController.navigate(Screen.MemberDetail.createRoute(memberName, "", yearMonth, analysisType)) navController.navigate(Screen.MemberDetail.createRoute(memberName, "", startMonth, endMonth, analysisType))
} }
) )
} }
@ -114,51 +130,68 @@ fun MainNavigation(
route = Screen.CategoryDetail.route, route = Screen.CategoryDetail.route,
arguments = listOf( arguments = listOf(
navArgument("category") { type = NavType.StringType }, navArgument("category") { type = NavType.StringType },
navArgument("yearMonth") { type = NavType.StringType } navArgument("startMonth") { type = NavType.StringType },
navArgument("endMonth") { type = NavType.StringType }
) )
) { backStackEntry -> ) { backStackEntry ->
val category = backStackEntry.arguments?.getString("category") ?: return@composable val category = backStackEntry.arguments?.getString("category") ?: ""
val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable val startMonth = YearMonth.parse(
val yearMonth = YearMonth.parse(yearMonthStr) backStackEntry.arguments?.getString("startMonth") ?: "",
DateTimeFormatter.ofPattern("yyyy-MM")
)
val endMonth = YearMonth.parse(
backStackEntry.arguments?.getString("endMonth") ?: "",
DateTimeFormatter.ofPattern("yyyy-MM")
)
CategoryDetailScreen( CategoryDetailScreen(
category = category, category = category,
yearMonth = yearMonth, startMonth = startMonth,
endMonth = endMonth,
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToMemberDetail = { memberName -> 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( composable(
route = Screen.MemberDetail.route, route = Screen.MemberDetail.route,
arguments = listOf( arguments = listOf(
navArgument("memberName") { type = NavType.StringType }, navArgument("memberName") { type = NavType.StringType },
navArgument("category") { type = NavType.StringType }, navArgument("category") { type = NavType.StringType },
navArgument("yearMonth") { type = NavType.StringType }, navArgument("startMonth") { type = NavType.StringType },
navArgument("type") { navArgument("endMonth") { type = NavType.StringType },
navArgument("type") {
type = NavType.StringType type = NavType.StringType
defaultValue = AnalysisType.EXPENSE.name defaultValue = AnalysisType.EXPENSE.name
} }
) )
) { backStackEntry -> ) { backStackEntry ->
val memberName = backStackEntry.arguments?.getString("memberName") ?: return@composable val memberName = backStackEntry.arguments?.getString("memberName") ?: ""
val category = backStackEntry.arguments?.getString("category") ?: return@composable val category = backStackEntry.arguments?.getString("category") ?: ""
val yearMonthStr = backStackEntry.arguments?.getString("yearMonth") ?: return@composable val startMonth = YearMonth.parse(
val yearMonth = YearMonth.parse(yearMonthStr) backStackEntry.arguments?.getString("startMonth") ?: "",
val type = backStackEntry.arguments?.getString("type")?.let { DateTimeFormatter.ofPattern("yyyy-MM")
try { )
AnalysisType.valueOf(it) val endMonth = YearMonth.parse(
} catch (e: IllegalArgumentException) { backStackEntry.arguments?.getString("endMonth") ?: "",
AnalysisType.EXPENSE DateTimeFormatter.ofPattern("yyyy-MM")
} )
} ?: AnalysisType.EXPENSE val type = AnalysisType.valueOf(
backStackEntry.arguments?.getString("type") ?: AnalysisType.EXPENSE.name
)
MemberDetailScreen( MemberDetailScreen(
memberName = memberName, memberName = memberName,
yearMonth = yearMonth,
category = category, category = category,
startMonth = startMonth,
endMonth = endMonth,
analysisType = type, analysisType = type,
onNavigateBack = { navController.popBackStack() } onNavigateBack = { navController.popBackStack() }
) )

View File

@ -48,8 +48,9 @@ enum class ViewMode {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AnalysisScreen( fun AnalysisScreen(
onNavigateToCategoryDetail: (String, YearMonth) -> Unit, onNavigateToCategoryDetail: (String, YearMonth, YearMonth) -> Unit,
onNavigateToMemberDetail: (String, YearMonth, AnalysisType) -> Unit onNavigateToMemberDetail: (String, YearMonth, YearMonth, AnalysisType) -> Unit,
modifier: Modifier = Modifier
) { ) {
val viewModel: AnalysisViewModel = viewModel() val viewModel: AnalysisViewModel = viewModel()
val startMonth by viewModel.startMonth.collectAsState() val startMonth by viewModel.startMonth.collectAsState()
@ -61,11 +62,13 @@ fun AnalysisScreen(
var showViewModeMenu by remember { mutableStateOf(false) } var showViewModeMenu by remember { mutableStateOf(false) }
var currentViewMode by rememberSaveable { mutableStateOf(ViewMode.CATEGORY) } var currentViewMode by rememberSaveable { mutableStateOf(ViewMode.CATEGORY) }
Scaffold { padding -> Scaffold(
modifier = modifier.fillMaxSize()
) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(paddingValues)
) { ) {
// 时间区间选择 // 时间区间选择
DateRangePicker( DateRangePicker(
@ -151,9 +154,9 @@ fun AnalysisScreen(
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
onCategoryClick = { category -> onCategoryClick = { category ->
if (currentViewMode == ViewMode.CATEGORY) { if (currentViewMode == ViewMode.CATEGORY) {
onNavigateToCategoryDetail(category, startMonth) onNavigateToCategoryDetail(category, startMonth, endMonth)
} else { } else {
onNavigateToMemberDetail(category, startMonth, selectedAnalysisType) onNavigateToMemberDetail(category, startMonth, endMonth, selectedAnalysisType)
} }
} }
) )
@ -169,9 +172,9 @@ fun AnalysisScreen(
stat = stat, stat = stat,
onClick = { onClick = {
if (currentViewMode == ViewMode.CATEGORY && category != null) { if (currentViewMode == ViewMode.CATEGORY && category != null) {
onNavigateToCategoryDetail(category, startMonth) onNavigateToCategoryDetail(category, startMonth, endMonth)
} else if (currentViewMode == ViewMode.MEMBER && member != null) { } else if (currentViewMode == ViewMode.MEMBER && member != null) {
onNavigateToMemberDetail(member, startMonth, selectedAnalysisType) onNavigateToMemberDetail(member, startMonth, endMonth, selectedAnalysisType)
} }
} }
) )

View File

@ -1,5 +1,6 @@
package com.yovinchen.bookkeeping.ui.screen package com.yovinchen.bookkeeping.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -17,6 +18,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -33,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.MemberStat
import com.yovinchen.bookkeeping.model.TransactionType import com.yovinchen.bookkeeping.model.TransactionType
import com.yovinchen.bookkeeping.ui.components.CategoryPieChart import com.yovinchen.bookkeeping.ui.components.CategoryPieChart
import com.yovinchen.bookkeeping.ui.components.RecordItem import com.yovinchen.bookkeeping.ui.components.RecordItem
@ -47,17 +50,20 @@ import java.util.Locale
@Composable @Composable
fun CategoryDetailScreen( fun CategoryDetailScreen(
category: String, category: String,
yearMonth: YearMonth, startMonth: YearMonth,
endMonth: YearMonth,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToMemberDetail: (String) -> Unit, onNavigateToMemberDetail: (String) -> Unit,
viewModel: CategoryDetailViewModel = viewModel(
factory = CategoryDetailViewModelFactory(
database = BookkeepingDatabase.getDatabase(LocalContext.current),
category = category,
startMonth = startMonth,
endMonth = endMonth
)
),
modifier: Modifier = Modifier 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 records by viewModel.records.collectAsState()
val memberStats by viewModel.memberStats.collectAsState() val memberStats by viewModel.memberStats.collectAsState()
val total by viewModel.total.collectAsState() val total by viewModel.total.collectAsState()
@ -107,16 +113,16 @@ fun CategoryDetailScreen(
} }
} }
// 第二部分:扇形图 // 第二部分:成员统计
item { item {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(16.dp)
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.padding(16.dp), .padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@ -125,19 +131,33 @@ fun CategoryDetailScreen(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
// 饼状图
CategoryPieChart( CategoryPieChart(
categoryData = memberStats.map { Pair(it.category, it.percentage.toFloat()) }, categoryData = emptyList(),
memberData = emptyList(), memberData = memberStats.map { Pair(it.member, it.percentage.toFloat()) },
currentViewMode = false, currentViewMode = true,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(200.dp), .height(200.dp),
onCategoryClick = { memberName -> 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 @Composable
private fun RecordItem( private fun RecordItem(
record: BookkeepingRecord, record: BookkeepingRecord,

View File

@ -44,7 +44,8 @@ import java.util.Locale
@Composable @Composable
fun MemberDetailScreen( fun MemberDetailScreen(
memberName: String, memberName: String,
yearMonth: YearMonth, startMonth: YearMonth,
endMonth: YearMonth,
category: String = "", category: String = "",
analysisType: AnalysisType = AnalysisType.EXPENSE, analysisType: AnalysisType = AnalysisType.EXPENSE,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
@ -53,8 +54,14 @@ fun MemberDetailScreen(
val records by viewModel.memberRecords.collectAsState(initial = emptyList()) val records by viewModel.memberRecords.collectAsState(initial = emptyList())
val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0) val totalAmount by viewModel.totalAmount.collectAsState(initial = 0.0)
LaunchedEffect(memberName, category, yearMonth, analysisType) { LaunchedEffect(memberName, category, startMonth, endMonth, analysisType) {
viewModel.loadMemberRecords(memberName, category, yearMonth, analysisType) viewModel.loadMemberRecords(
memberName = memberName,
category = category,
startMonth = startMonth,
endMonth = endMonth,
analysisType = analysisType
)
} }
val groupedRecords = remember(records) { val groupedRecords = remember(records) {

View File

@ -5,24 +5,25 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.yovinchen.bookkeeping.data.BookkeepingDatabase import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.CategoryStat import com.yovinchen.bookkeeping.model.MemberStat
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import java.time.YearMonth import java.time.YearMonth
import java.time.format.DateTimeFormatter import java.time.ZoneId
import java.util.Date
class CategoryDetailViewModel( class CategoryDetailViewModel(
private val database: BookkeepingDatabase, private val database: BookkeepingDatabase,
private val category: String, private val category: String,
private val month: YearMonth private val startMonth: YearMonth,
private val endMonth: YearMonth
) : ViewModel() { ) : ViewModel() {
private val recordDao = database.bookkeepingDao() private val recordDao = database.bookkeepingDao()
private val yearMonthStr = month.format(DateTimeFormatter.ofPattern("yyyy-MM"))
private val _records = MutableStateFlow<List<BookkeepingRecord>>(emptyList()) private val _records = MutableStateFlow<List<BookkeepingRecord>>(emptyList())
val records: StateFlow<List<BookkeepingRecord>> = _records.asStateFlow() val records: StateFlow<List<BookkeepingRecord>> = _records.asStateFlow()
private val _memberStats = MutableStateFlow<List<CategoryStat>>(emptyList()) private val _memberStats = MutableStateFlow<List<MemberStat>>(emptyList())
val memberStats: StateFlow<List<CategoryStat>> = _memberStats.asStateFlow() val memberStats: StateFlow<List<MemberStat>> = _memberStats.asStateFlow()
val total: StateFlow<Double> = records val total: StateFlow<Double> = records
.map { records -> records.sumOf { it.amount } } .map { records -> records.sumOf { it.amount } }
@ -33,22 +34,30 @@ class CategoryDetailViewModel(
) )
init { init {
recordDao.getRecordsByCategory(category) val startDate = startMonth.atDay(1).atStartOfDay()
.onEach { records -> .atZone(ZoneId.systemDefault())
_records.value = records.filter { record -> .toInstant()
val recordMonth = YearMonth.from( .let { Date.from(it) }
DateTimeFormatter.ofPattern("yyyy-MM")
.parse(yearMonthStr)
)
YearMonth.from(record.date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime()) == recordMonth
}
}
.launchIn(viewModelScope)
recordDao.getMemberStatsByCategory(category, yearMonthStr) val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59)
.onEach { stats -> .atZone(ZoneId.systemDefault())
_memberStats.value = stats .toInstant()
} .let { Date.from(it) }
.launchIn(viewModelScope)
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)
} }
} }

View File

@ -8,12 +8,13 @@ import java.time.YearMonth
class CategoryDetailViewModelFactory( class CategoryDetailViewModelFactory(
private val database: BookkeepingDatabase, private val database: BookkeepingDatabase,
private val category: String, private val category: String,
private val month: YearMonth private val startMonth: YearMonth,
private val endMonth: YearMonth
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) { if (modelClass.isAssignableFrom(CategoryDetailViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return CategoryDetailViewModel(database, category, month) as T return CategoryDetailViewModel(database, category, startMonth, endMonth) as T
} }
throw IllegalArgumentException("Unknown ViewModel class") throw IllegalArgumentException("Unknown ViewModel class")
} }

View File

@ -7,9 +7,7 @@ import com.yovinchen.bookkeeping.data.BookkeepingDatabase
import com.yovinchen.bookkeeping.model.BookkeepingRecord import com.yovinchen.bookkeeping.model.BookkeepingRecord
import com.yovinchen.bookkeeping.model.AnalysisType import com.yovinchen.bookkeeping.model.AnalysisType
import com.yovinchen.bookkeeping.model.TransactionType import com.yovinchen.bookkeeping.model.TransactionType
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.time.YearMonth import java.time.YearMonth
import java.time.ZoneId import java.time.ZoneId
import java.util.Date import java.util.Date
@ -19,47 +17,56 @@ class MemberDetailViewModel(application: Application) : AndroidViewModel(applica
private val recordDao = database.bookkeepingDao() private val recordDao = database.bookkeepingDao()
private val _memberRecords = MutableStateFlow<List<BookkeepingRecord>>(emptyList()) private val _memberRecords = MutableStateFlow<List<BookkeepingRecord>>(emptyList())
val memberRecords: StateFlow<List<BookkeepingRecord>> = _memberRecords val memberRecords: StateFlow<List<BookkeepingRecord>> = _memberRecords.asStateFlow()
private val _totalAmount = MutableStateFlow(0.0) private val _totalAmount = MutableStateFlow(0.0)
val totalAmount: StateFlow<Double> = _totalAmount val totalAmount: StateFlow<Double> = _totalAmount.asStateFlow()
fun loadMemberRecords(memberName: String, category: String, yearMonth: YearMonth, analysisType: AnalysisType) { fun loadMemberRecords(
viewModelScope.launch { memberName: String,
val startDate = yearMonth.atDay(1).atStartOfDay() category: String,
.atZone(ZoneId.systemDefault()) startMonth: YearMonth,
.toInstant() endMonth: YearMonth,
.let { Date.from(it) } analysisType: AnalysisType
) {
val startDate = startMonth.atDay(1).atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
.let { Date.from(it) }
val endDate = yearMonth.atEndOfMonth().atTime(23, 59, 59) val endDate = endMonth.atEndOfMonth().atTime(23, 59, 59)
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
.toInstant() .toInstant()
.let { Date.from(it) } .let { Date.from(it) }
val transactionType = when (analysisType) { val transactionType = when (analysisType) {
AnalysisType.INCOME -> TransactionType.INCOME AnalysisType.INCOME -> TransactionType.INCOME
AnalysisType.EXPENSE -> TransactionType.EXPENSE AnalysisType.EXPENSE -> TransactionType.EXPENSE
else -> null 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 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)
} }
} }